guohanghui commited on
Commit
165b523
·
verified ·
1 Parent(s): e8432ba

Upload 199 files

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .gitattributes +4 -0
  2. Dockerfile +18 -0
  3. README.md +27 -5
  4. app.py +45 -0
  5. causalml/mcp_output/README_MCP.md +73 -0
  6. causalml/mcp_output/analysis.json +459 -0
  7. causalml/mcp_output/diff_report.md +60 -0
  8. causalml/mcp_output/mcp_plugin/__init__.py +0 -0
  9. causalml/mcp_output/mcp_plugin/adapter.py +214 -0
  10. causalml/mcp_output/mcp_plugin/main.py +13 -0
  11. causalml/mcp_output/mcp_plugin/mcp_service.py +164 -0
  12. causalml/mcp_output/requirements.txt +26 -0
  13. causalml/mcp_output/start_mcp.py +30 -0
  14. causalml/mcp_output/workflow_summary.json +201 -0
  15. causalml/source/.pre-commit-config.yaml +11 -0
  16. causalml/source/.readthedocs.yml +25 -0
  17. causalml/source/ANTITRUST.md +7 -0
  18. causalml/source/CHARTER.md +49 -0
  19. causalml/source/CODE_OF_CONDUCT.md +75 -0
  20. causalml/source/CONTRIBUTING.md +134 -0
  21. causalml/source/GOVERNANCE.md +54 -0
  22. causalml/source/LICENSE +13 -0
  23. causalml/source/MAINTAINERS.md +27 -0
  24. causalml/source/MANIFEST.in +7 -0
  25. causalml/source/Makefile +27 -0
  26. causalml/source/README.md +132 -0
  27. causalml/source/SECURITY.md +11 -0
  28. causalml/source/STEERING_COMMITTEE.md +14 -0
  29. causalml/source/TRADEMARKS.md +44 -0
  30. causalml/source/__init__.py +4 -0
  31. causalml/source/causalml/__init__.py +10 -0
  32. causalml/source/causalml/dataset/__init__.py +16 -0
  33. causalml/source/causalml/dataset/classification.py +692 -0
  34. causalml/source/causalml/dataset/regression.py +209 -0
  35. causalml/source/causalml/dataset/semiSynthetic.py +1056 -0
  36. causalml/source/causalml/dataset/synthetic.py +655 -0
  37. causalml/source/causalml/feature_selection/__init__.py +1 -0
  38. causalml/source/causalml/feature_selection/filters.py +663 -0
  39. causalml/source/causalml/features.py +267 -0
  40. causalml/source/causalml/inference/__init__.py +0 -0
  41. causalml/source/causalml/inference/iv/__init__.py +2 -0
  42. causalml/source/causalml/inference/iv/drivlearner.py +881 -0
  43. causalml/source/causalml/inference/iv/iv_regression.py +48 -0
  44. causalml/source/causalml/inference/meta/__init__.py +12 -0
  45. causalml/source/causalml/inference/meta/base.py +337 -0
  46. causalml/source/causalml/inference/meta/drlearner.py +592 -0
  47. causalml/source/causalml/inference/meta/explainer.py +278 -0
  48. causalml/source/causalml/inference/meta/rlearner.py +695 -0
  49. causalml/source/causalml/inference/meta/slearner.py +411 -0
  50. causalml/source/causalml/inference/meta/tlearner.py +423 -0
.gitattributes CHANGED
@@ -33,3 +33,7 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ causalml/source/docs/_static/img/synthetic_dgp_scatter_plot.png filter=lfs diff=lfs merge=lfs -text
37
+ causalml/source/docs/_static/img/uplift_tree_vis.png filter=lfs diff=lfs merge=lfs -text
38
+ causalml/source/docs/examples/causal_trees_with_synthetic_data_multiple_treatment_groups.ipynb filter=lfs diff=lfs merge=lfs -text
39
+ causalml/source/docs/examples/causal_trees_with_synthetic_data.ipynb filter=lfs diff=lfs merge=lfs -text
Dockerfile ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.10
2
+
3
+ RUN useradd -m -u 1000 user && python -m pip install --upgrade pip
4
+ USER user
5
+ ENV PATH="/home/user/.local/bin:$PATH"
6
+
7
+ WORKDIR /app
8
+
9
+ COPY --chown=user ./requirements.txt requirements.txt
10
+ RUN pip install --no-cache-dir --upgrade -r requirements.txt
11
+
12
+ COPY --chown=user . /app
13
+ ENV MCP_TRANSPORT=http
14
+ ENV MCP_PORT=7860
15
+
16
+ EXPOSE 7860
17
+
18
+ CMD ["python", "causalml/mcp_output/start_mcp.py"]
README.md CHANGED
@@ -1,10 +1,32 @@
1
  ---
2
- title: Causalml
3
- emoji: 📊
4
- colorFrom: pink
5
- colorTo: red
6
  sdk: docker
 
 
7
  pinned: false
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: Causalml MCP
3
+ emoji: 🤖
4
+ colorFrom: blue
5
+ colorTo: purple
6
  sdk: docker
7
+ sdk_version: "4.26.0"
8
+ app_file: app.py
9
  pinned: false
10
  ---
11
 
12
+ # Causalml MCP Service
13
+
14
+ Auto-generated MCP service for causalml.
15
+
16
+ ## Usage
17
+
18
+ ```
19
+ https://None-causalml-mcp.hf.space/mcp
20
+ ```
21
+
22
+ ## Connect with Cursor
23
+
24
+ ```json
25
+ {
26
+ "mcpServers": {
27
+ "causalml": {
28
+ "url": "https://None-causalml-mcp.hf.space/mcp"
29
+ }
30
+ }
31
+ }
32
+ ```
app.py ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI
2
+ import os
3
+ import sys
4
+
5
+ mcp_plugin_path = os.path.join(os.path.dirname(__file__), "causalml", "mcp_output", "mcp_plugin")
6
+ sys.path.insert(0, mcp_plugin_path)
7
+
8
+ app = FastAPI(
9
+ title="Causalml MCP Service",
10
+ description="Auto-generated MCP service for causalml",
11
+ version="1.0.0"
12
+ )
13
+
14
+ @app.get("/")
15
+ def root():
16
+ return {
17
+ "service": "Causalml MCP Service",
18
+ "version": "1.0.0",
19
+ "status": "running",
20
+ "transport": os.environ.get("MCP_TRANSPORT", "http")
21
+ }
22
+
23
+ @app.get("/health")
24
+ def health_check():
25
+ return {"status": "healthy", "service": "causalml MCP"}
26
+
27
+ @app.get("/tools")
28
+ def list_tools():
29
+ try:
30
+ from mcp_service import create_app
31
+ mcp_app = create_app()
32
+ tools = []
33
+ for tool_name, tool_func in mcp_app.tools.items():
34
+ tools.append({
35
+ "name": tool_name,
36
+ "description": tool_func.__doc__ or "No description available"
37
+ })
38
+ return {"tools": tools}
39
+ except Exception as e:
40
+ return {"error": f"Failed to load tools: {str(e)}"}
41
+
42
+ if __name__ == "__main__":
43
+ import uvicorn
44
+ port = int(os.environ.get("PORT", 7860))
45
+ uvicorn.run(app, host="0.0.0.0", port=port)
causalml/mcp_output/README_MCP.md ADDED
@@ -0,0 +1,73 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # CausalML: Model Context Protocol (MCP) Service
2
+
3
+ ## Project Introduction
4
+
5
+ CausalML is a comprehensive Python package designed to provide a suite of uplift modeling and causal inference methods using machine learning algorithms. It is based on recent research and offers a standard interface for estimating the Conditional Average Treatment Effect (CATE) and Individual Treatment Effect (ITE) from experimental or observational data. The package is particularly valuable for real-world applications such as campaign targeting optimization and personalized engagement by estimating the causal impact of interventions on outcomes.
6
+
7
+ ## Installation Method
8
+
9
+ To install CausalML, ensure you have the following dependencies:
10
+
11
+ - scikit-learn>=1.6.0
12
+ - xgboost
13
+ - tensorflow>=2.4.0
14
+ - torch
15
+ - scipy>=1.4.1
16
+ - pandas>=0.24.1
17
+ - setuptools
18
+ - Cython
19
+
20
+ Optional dependencies include:
21
+
22
+ - pyro-ppl
23
+ - cibuildwheel
24
+ - pytest
25
+ - pytest-cov
26
+
27
+ You can install CausalML using pip:
28
+
29
+ ```
30
+ pip install causalml
31
+ ```
32
+
33
+ ## Quick Start
34
+
35
+ To quickly get started with CausalML, you can use the following example to estimate treatment effects:
36
+
37
+ 1. Import the necessary modules.
38
+ 2. Load your dataset.
39
+ 3. Choose a meta-learner or inference method.
40
+ 4. Fit the model and predict treatment effects.
41
+
42
+ Example:
43
+
44
+ ```
45
+ from causalml.inference.meta import BaseTLearner
46
+ from causalml.dataset import make_uplift_classification
47
+
48
+ X, treatment, y = make_uplift_classification()
49
+ learner = BaseTLearner()
50
+ learner.fit(X, treatment, y)
51
+ ate = learner.estimate_ate(X, treatment, y)
52
+ ```
53
+
54
+ ## Available Tools and Endpoints List
55
+
56
+ - **Meta-Learners**: Includes BaseSLearner, BaseTLearner, BaseXLearner, BaseRLearner, BaseDRLearner, and TMLELearner for various strategies in estimating treatment effects.
57
+ - **Tree-Based Methods**: UpliftTreeClassifier, UpliftRandomForestClassifier, CausalTreeRegressor, and CausalRandomForestRegressor for uplift modeling and causal inference.
58
+ - **Neural Network Methods**: DragonNet and CEVAE for causal inference using TensorFlow and PyTorch.
59
+ - **Instrumental Variable Methods**: DRIVLearner for causal inference.
60
+ - **Metrics and Visualization**: Functions like AUUC, Qini, and plot_lift for evaluating causal inference models.
61
+ - **Optimization Methods**: CounterfactualUnitSelector and CounterfactualValueEstimator for treatment effect estimation and counterfactual analysis.
62
+
63
+ ## Common Issues and Notes
64
+
65
+ - Ensure all dependencies are correctly installed to avoid import errors.
66
+ - Performance may vary based on the dataset size and complexity of the model chosen.
67
+ - For optimal performance, consider using Cython extensions and leveraging GPU support with TensorFlow or PyTorch.
68
+
69
+ ## Reference Links or Documentation
70
+
71
+ For more detailed documentation and examples, visit the [CausalML GitHub repository](https://github.com/uber/causalml).
72
+
73
+ For additional information on methodology and usage, refer to the documentation files within the repository, such as `docs/methodology.rst` and `README.md`.
causalml/mcp_output/analysis.json ADDED
@@ -0,0 +1,459 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "summary": {
3
+ "repository_url": "https://github.com/uber/causalml",
4
+ "summary": "Imported via zip fallback, file count: 102",
5
+ "file_tree": {
6
+ ".github/.stale.yml": {
7
+ "size": 683
8
+ },
9
+ ".github/ISSUE_TEMPLATE/bug_report.md": {
10
+ "size": 730
11
+ },
12
+ ".github/ISSUE_TEMPLATE/feature_request.md": {
13
+ "size": 604
14
+ },
15
+ ".github/PULL_REQUEST_TEMPLATE.md": {
16
+ "size": 1616
17
+ },
18
+ ".github/workflows/black.yml": {
19
+ "size": 154
20
+ },
21
+ ".github/workflows/python-publish.yml": {
22
+ "size": 1998
23
+ },
24
+ ".github/workflows/python-test.yaml": {
25
+ "size": 963
26
+ },
27
+ ".github/workflows/test-build-from-source.yml": {
28
+ "size": 2193
29
+ },
30
+ ".github/workflows/test-pypi-install.yml": {
31
+ "size": 1209
32
+ },
33
+ ".pre-commit-config.yaml": {
34
+ "size": 261
35
+ },
36
+ ".readthedocs.yml": {
37
+ "size": 523
38
+ },
39
+ "ANTITRUST.md": {
40
+ "size": 1206
41
+ },
42
+ "CHARTER.md": {
43
+ "size": 4155
44
+ },
45
+ "CODE_OF_CONDUCT.md": {
46
+ "size": 3224
47
+ },
48
+ "CONTRIBUTING.md": {
49
+ "size": 5638
50
+ },
51
+ "GOVERNANCE.md": {
52
+ "size": 3908
53
+ },
54
+ "MAINTAINERS.md": {
55
+ "size": 1310
56
+ },
57
+ "README.md": {
58
+ "size": 9061
59
+ },
60
+ "SECURITY.md": {
61
+ "size": 227
62
+ },
63
+ "STEERING_COMMITTEE.md": {
64
+ "size": 1098
65
+ },
66
+ "TRADEMARKS.md": {
67
+ "size": 4790
68
+ },
69
+ "causalml/__init__.py": {
70
+ "size": 149
71
+ },
72
+ "causalml/dataset/__init__.py": {
73
+ "size": 872
74
+ },
75
+ "causalml/dataset/classification.py": {
76
+ "size": 28659
77
+ },
78
+ "causalml/dataset/regression.py": {
79
+ "size": 8860
80
+ },
81
+ "causalml/dataset/semiSynthetic.py": {
82
+ "size": 36472
83
+ },
84
+ "causalml/dataset/synthetic.py": {
85
+ "size": 24403
86
+ },
87
+ "causalml/feature_selection/__init__.py": {
88
+ "size": 34
89
+ },
90
+ "causalml/feature_selection/filters.py": {
91
+ "size": 27676
92
+ },
93
+ "causalml/features.py": {
94
+ "size": 8366
95
+ },
96
+ "causalml/inference/__init__.py": {
97
+ "size": 0
98
+ },
99
+ "causalml/inference/iv/__init__.py": {
100
+ "size": 117
101
+ },
102
+ "causalml/inference/iv/drivlearner.py": {
103
+ "size": 37605
104
+ },
105
+ "causalml/inference/iv/iv_regression.py": {
106
+ "size": 1420
107
+ },
108
+ "causalml/inference/meta/__init__.py": {
109
+ "size": 474
110
+ },
111
+ "causalml/inference/meta/base.py": {
112
+ "size": 13510
113
+ },
114
+ "causalml/inference/meta/drlearner.py": {
115
+ "size": 24255
116
+ },
117
+ "causalml/inference/meta/explainer.py": {
118
+ "size": 11472
119
+ },
120
+ "causalml/inference/meta/rlearner.py": {
121
+ "size": 29178
122
+ },
123
+ "causalml/inference/meta/slearner.py": {
124
+ "size": 15873
125
+ },
126
+ "causalml/inference/meta/tlearner.py": {
127
+ "size": 15711
128
+ },
129
+ "causalml/inference/meta/tmle.py": {
130
+ "size": 8014
131
+ },
132
+ "causalml/inference/meta/utils.py": {
133
+ "size": 4276
134
+ },
135
+ "causalml/inference/meta/xlearner.py": {
136
+ "size": 26635
137
+ },
138
+ "causalml/inference/tf/__init__.py": {
139
+ "size": 33
140
+ },
141
+ "causalml/inference/tf/dragonnet.py": {
142
+ "size": 10594
143
+ },
144
+ "causalml/inference/tf/utils.py": {
145
+ "size": 6098
146
+ },
147
+ "causalml/inference/torch/__init__.py": {
148
+ "size": 25
149
+ },
150
+ "causalml/inference/torch/cevae.py": {
151
+ "size": 5470
152
+ },
153
+ "causalml/inference/tree/__init__.py": {
154
+ "size": 423
155
+ },
156
+ "causalml/inference/tree/_tree/__init__.py": {
157
+ "size": 302
158
+ },
159
+ "causalml/inference/tree/_tree/_classes.py": {
160
+ "size": 24699
161
+ },
162
+ "causalml/inference/tree/causal/__init__.py": {
163
+ "size": 0
164
+ },
165
+ "causalml/inference/tree/causal/_tree.py": {
166
+ "size": 10355
167
+ },
168
+ "causalml/inference/tree/causal/causalforest.py": {
169
+ "size": 20212
170
+ },
171
+ "causalml/inference/tree/causal/causaltree.py": {
172
+ "size": 17840
173
+ },
174
+ "causalml/inference/tree/plot.py": {
175
+ "size": 24071
176
+ },
177
+ "causalml/inference/tree/utils.py": {
178
+ "size": 11016
179
+ },
180
+ "causalml/match.py": {
181
+ "size": 20210
182
+ },
183
+ "causalml/metrics/__init__.py": {
184
+ "size": 743
185
+ },
186
+ "causalml/metrics/classification.py": {
187
+ "size": 975
188
+ },
189
+ "causalml/metrics/const.py": {
190
+ "size": 12
191
+ },
192
+ "causalml/metrics/regression.py": {
193
+ "size": 3193
194
+ },
195
+ "causalml/metrics/sensitivity.py": {
196
+ "size": 22273
197
+ },
198
+ "causalml/metrics/visualize.py": {
199
+ "size": 36312
200
+ },
201
+ "causalml/optimize/__init__.py": {
202
+ "size": 263
203
+ },
204
+ "causalml/optimize/pns.py": {
205
+ "size": 2765
206
+ },
207
+ "causalml/optimize/policylearner.py": {
208
+ "size": 5973
209
+ },
210
+ "causalml/optimize/unit_selection.py": {
211
+ "size": 9417
212
+ },
213
+ "causalml/optimize/utils.py": {
214
+ "size": 4210
215
+ },
216
+ "causalml/optimize/value_optimization.py": {
217
+ "size": 4094
218
+ },
219
+ "causalml/propensity.py": {
220
+ "size": 7229
221
+ },
222
+ "docs/conf.py": {
223
+ "size": 9212
224
+ },
225
+ "docs/environment-py311-rtd.yml": {
226
+ "size": 600
227
+ },
228
+ "docs/environment-py39-rtd.yml": {
229
+ "size": 605
230
+ },
231
+ "docs/issue-859-resolution.md": {
232
+ "size": 1016
233
+ },
234
+ "docs/plans/2026-01-30-scipy-1.16-support.md": {
235
+ "size": 11647
236
+ },
237
+ "docs/requirements.txt": {
238
+ "size": 114
239
+ },
240
+ "pyproject.toml": {
241
+ "size": 1505
242
+ },
243
+ "setup.cfg": {
244
+ "size": 660
245
+ },
246
+ "setup.py": {
247
+ "size": 1759
248
+ },
249
+ "tests/__init__.py": {
250
+ "size": 0
251
+ },
252
+ "tests/conftest.py": {
253
+ "size": 2622
254
+ },
255
+ "tests/const.py": {
256
+ "size": 392
257
+ },
258
+ "tests/test_causal_trees.py": {
259
+ "size": 9991
260
+ },
261
+ "tests/test_cevae.py": {
262
+ "size": 1490
263
+ },
264
+ "tests/test_counterfactual_unit_selection.py": {
265
+ "size": 2475
266
+ },
267
+ "tests/test_datasets.py": {
268
+ "size": 2379
269
+ },
270
+ "tests/test_dragonnet.py": {
271
+ "size": 661
272
+ },
273
+ "tests/test_feature_selection.py": {
274
+ "size": 2034
275
+ },
276
+ "tests/test_features.py": {
277
+ "size": 1632
278
+ },
279
+ "tests/test_ivlearner.py": {
280
+ "size": 2168
281
+ },
282
+ "tests/test_match.py": {
283
+ "size": 3026
284
+ },
285
+ "tests/test_meta_learners.py": {
286
+ "size": 34264
287
+ },
288
+ "tests/test_metrics.py": {
289
+ "size": 1074
290
+ },
291
+ "tests/test_propensity.py": {
292
+ "size": 1629
293
+ },
294
+ "tests/test_sensitivity.py": {
295
+ "size": 6687
296
+ },
297
+ "tests/test_uplift_trees.py": {
298
+ "size": 11315
299
+ },
300
+ "tests/test_utils.py": {
301
+ "size": 692
302
+ },
303
+ "tests/test_value_optimization.py": {
304
+ "size": 2735
305
+ },
306
+ "tests/test_visualize.py": {
307
+ "size": 1768
308
+ },
309
+ "tox.ini": {
310
+ "size": 307
311
+ }
312
+ },
313
+ "processed_by": "zip_fallback",
314
+ "success": true
315
+ },
316
+ "structure": {
317
+ "packages": [
318
+ "source.causalml",
319
+ "source.causalml.dataset",
320
+ "source.causalml.feature_selection",
321
+ "source.causalml.inference",
322
+ "source.causalml.metrics",
323
+ "source.causalml.optimize",
324
+ "source.tests"
325
+ ]
326
+ },
327
+ "dependencies": {
328
+ "has_environment_yml": false,
329
+ "has_requirements_txt": false,
330
+ "pyproject": true,
331
+ "setup_cfg": true,
332
+ "setup_py": true
333
+ },
334
+ "entry_points": {
335
+ "imports": [],
336
+ "cli": [],
337
+ "modules": []
338
+ },
339
+ "llm_analysis": {
340
+ "core_modules": [
341
+ {
342
+ "package": "source.causalml.inference.meta",
343
+ "module": "meta",
344
+ "functions": [
345
+ "fit",
346
+ "predict",
347
+ "estimate_ate"
348
+ ],
349
+ "classes": [
350
+ "BaseSLearner",
351
+ "BaseTLearner",
352
+ "BaseXLearner",
353
+ "BaseRLearner",
354
+ "BaseDRLearner",
355
+ "TMLELearner"
356
+ ],
357
+ "description": "Meta-learners for causal inference, providing various strategies for estimating treatment effects."
358
+ },
359
+ {
360
+ "package": "source.causalml.inference.tree",
361
+ "module": "tree",
362
+ "functions": [],
363
+ "classes": [
364
+ "UpliftTreeClassifier",
365
+ "UpliftRandomForestClassifier",
366
+ "CausalTreeRegressor",
367
+ "CausalRandomForestRegressor"
368
+ ],
369
+ "description": "Tree-based methods for uplift modeling and causal inference, implemented with Cython for performance."
370
+ },
371
+ {
372
+ "package": "source.causalml.inference.nn",
373
+ "module": "nn",
374
+ "functions": [],
375
+ "classes": [
376
+ "DragonNet",
377
+ "CEVAE"
378
+ ],
379
+ "description": "Neural network methods for causal inference, leveraging TensorFlow and PyTorch."
380
+ },
381
+ {
382
+ "package": "source.causalml.inference.iv",
383
+ "module": "iv",
384
+ "functions": [],
385
+ "classes": [
386
+ "DRIVLearner"
387
+ ],
388
+ "description": "Instrumental variable methods for causal inference."
389
+ },
390
+ {
391
+ "package": "source.causalml.metrics",
392
+ "module": "metrics",
393
+ "functions": [
394
+ "AUUC",
395
+ "Qini",
396
+ "plot_lift"
397
+ ],
398
+ "classes": [],
399
+ "description": "Metrics and visualization tools for evaluating causal inference models."
400
+ },
401
+ {
402
+ "package": "source.causalml.optimize",
403
+ "module": "optimize",
404
+ "functions": [],
405
+ "classes": [
406
+ "CounterfactualUnitSelector",
407
+ "CounterfactualValueEstimator"
408
+ ],
409
+ "description": "Optimization methods for treatment effect estimation and counterfactual analysis."
410
+ }
411
+ ],
412
+ "cli_commands": [],
413
+ "import_strategy": {
414
+ "primary": "import",
415
+ "fallback": "blackbox",
416
+ "confidence": 0.9
417
+ },
418
+ "dependencies": {
419
+ "required": [
420
+ "scikit-learn>=1.6.0",
421
+ "xgboost",
422
+ "tensorflow>=2.4.0",
423
+ "torch",
424
+ "scipy>=1.4.1",
425
+ "pandas>=0.24.1",
426
+ "setuptools",
427
+ "Cython"
428
+ ],
429
+ "optional": [
430
+ "pyro-ppl",
431
+ "cibuildwheel",
432
+ "pytest",
433
+ "pytest-cov"
434
+ ]
435
+ },
436
+ "risk_assessment": {
437
+ "import_feasibility": 0.8,
438
+ "intrusiveness_risk": "medium",
439
+ "complexity": "complex"
440
+ }
441
+ },
442
+ "deepwiki_analysis": {
443
+ "repo_url": "https://github.com/uber/causalml",
444
+ "repo_name": "causalml",
445
+ "content": "uber/causalml\nInstallation and Setup\nPackage Structure\nDevelopment and Contributing\nCore Concepts and Methodology\nCausal Inference and Uplift Modeling\nTreatment Effect Estimation\nPropensity Scores\nInference Methods\nMeta-Learners\nS-Learner and T-Learner\nDR-Learner and DRIV-Learner\nTMLE Learner\nTree-Based Methods\nUplift Trees\nCausal Trees\nNeural Network Methods\nInstrumental Variables\nEvaluation and Interpretation\nMetrics and Visualization\nFeature Importance and Explainability\nSensitivity Analysis\nData Handling and Optimization\nSynthetic Data Generation\nFeature Selection\nMatching Methods\nTreatment Optimization\nExamples and Use Cases\nBasic Usage Examples\nAdvanced Applications\nDocumentation and Configuration\ncausalml/inference/meta/__init__.py\ndocs/about.rst\ndocs/methodology.rst\ndocs/refs.bib\npyproject.toml\nCausalML is a comprehensive Python package that provides a suite of uplift modeling and causal inference methods using machine learning algorithms based on recent research. It offers a standard interface for estimating the Conditional Average Treatment Effect (CATE) and Individual Treatment Effect (ITE) from experimental or observational data. The package estimates the causal impact of interventionTon outcomeYfor users with observed featuresX, without requiring strong assumptions on the model form.\nCausalML is particularly valuable for real-world applications including:\nCampaign targeting optimization: Identifying customers who will have favorable responses to advertising campaigns by estimating KPI effects from ad exposure at the individual level\nPersonalized engagement: Optimizing customer interactions across multiple treatment options (product choices, messaging channels) using heterogeneous treatment effect estimation\nThe library integrates seamlessly with the Python scientific computing ecosystem, building on established frameworks like scikit-learn, XGBoost, TensorFlow, and PyTorch while providing specialized causal inference capabilities not available in general-purpose ML libraries.\nSources:README.md18-28pyproject.toml1-5docs/about.rst4-7\nPackage Architecture\nCausalML is organized into a modular architecture with clear separation of concerns across different aspects of causal inference and uplift modeling.\nSystem Architecture Overview\nBuild SystemExternal DependenciesCausalML Library v0.15.5InfrastructureData & EvaluationCore Inference Enginecausalml.inference.metaS, T, X, R, DR, TMLE Learnerscausalml.inference.treeUplift Trees, Causal Treescausalml.inference.nnDragonNet, CEVAEcausalml.inference.iv2SLS, DRIV Learnercausalml.datasetmake_uplift_classificationcausalml.metricsAUUC, Qini, plot_liftcausalml.feature_selectionFilter methods, LR testcausalml.matchNearestNeighborMatchcausalml.propensityElasticNetPropensityModelcausalml.optimizeCounterfactualUnitSelectorscikit-learn>=1.6.0xgboosttensorflow>=2.4.0torch + pyro-pplscipy>=1.4.1pandas>=0.24.1setuptools + Cythoncibuildwheelpytest + pytest-cov\nBuild System\nExternal Dependencies\nCausalML Library v0.15.5\nInfrastructure\nData & Evaluation\nCore Inference Engine\ncausalml.inference.metaS, T, X, R, DR, TMLE Learners\ncausalml.inference.treeUplift Trees, Causal Trees\ncausalml.inference.nnDragonNet, CEVAE\ncausalml.inference.iv2SLS, DRIV Learner\ncausalml.datasetmake_uplift_classification\ncausalml.metricsAUUC, Qini, plot_lift\ncausalml.feature_selectionFilter methods, LR test\ncausalml.matchNearestNeighborMatch\ncausalml.propensityElasticNetPropensityModel\ncausalml.optimizeCounterfactualUnitSelector\nscikit-learn>=1.6.0\ntensorflow>=2.4.0\ntorch + pyro-ppl\nscipy>=1.4.1\npandas>=0.24.1\nsetuptools + Cython\ncibuildwheel\npytest + pytest-cov\nModule Dependency Analysis: The inference module forms the core engine with heavy integration to the Python ML ecosystem. Tree-based methods utilize Cython extensions for performance, while neural network methods have optional TensorFlow/PyTorch dependencies. The evaluation and infrastructure modules provide supporting functionality for complete causal inference workflows.\nSources:pyproject.toml24-76docs/methodology.rst10-36\nCore Module Structure\ncausalml.inference.meta\nBaseSLearner\nBaseTLearner\nBaseXLearner\nBaseRLearner\nBaseDRLearner\nTMLELearner\ncausalml.inference.tree\nUpliftTreeClassifier\nUpliftRandomForestClassifier\nCausalTreeRegressor\ncausalml.inference.nn\ncausalml.inference.iv\ncausalml.metrics\ncausalml.optimize\nCounterfactualUnitSelector\nCounterfactualValueEstimator\ncausalml.dataset\nmake_uplift_classification\nmake_uplift_regression\ncausalml.feature_selection\nFilterSelect\nLRSelectorRegressor\ncausalml.propensity\nElasticNetPropensityModel\nGradientBoostedPropensityModel\ncausalml.match\nNearestNeighborMatch\nMatchOptimizer\nSources:causalml/inference/meta/__init__.py1-13docs/methodology.rst10-36\nCore Modules and Their Purpose\nfeature_selection\nSources:causalml/__init__.py1-10docs/methodology.rst10-36\nInference Methods\nTheinferencemodule contains the core estimation algorithms for causal effects, organized into several submodules based on methodology.\nInference Methods Architecture\nThecausalml.inferencemodule contains the core estimation algorithms, organized into specialized submodules based on methodological approach.\ncausalml.inference\nMeta-Learner Class Hierarchy\nBaseSLearner\"Single model with treatment indicator\"+model: any ML model+fit(X, treatment, y)+predict(X, treatment)+estimate_ate(X, treatment, y)BaseTLearner\"Separate models per treatment\"+model_c: control model+model_t: treatment model+fit(X, treatment, y)+predict(X, treatment)+estimate_ate(X, treatment, y)BaseXLearner\"Four-stage cross-learning\"+model_c: control outcome model+model_t: treatment outcome model+model_tau_c: control effect model+model_tau_t: treatment effect model+propensity_model: ElasticNetPropensityModel+fit(X, treatment, y)+predict(X, treatment)BaseRLearner\"Residual-based approach\"+model_mu: outcome model+model_tau: treatment effect model+model_p: propensity model+cv: cross-validation folds+fit(X, treatment, y)+predict(X, treatment)BaseDRLearner\"Doubly robust estimation\"+model_mu_c: control outcome model+model_mu_t: treatment outcome model+model_tau: effect model+model_p: propensity model+cv: cross-validation strategy+fit(X, treatment, y)+predict(X, treatment)TMLELearner\"Targeted maximum likelihood\"+model_y: outcome model+model_p: propensity model+clip_bounds: [0.01, 0.99]+fit(X, treatment, y)+estimate_ate(X, treatment, y)BaseSRegressorBaseSClassifierLRSRegressorBaseTRegressorBaseTClassifierXGBTRegressorMLPTRegressorBaseXRegressorBaseXClassifierBaseRRegressorBaseRClassifierXGBRRegressorBaseDRRegressorBaseDRClassifierXGBDRRegressor\nBaseSLearner\n\"Single model with treatment indicator\"\n+model: any ML model\n+fit(X, treatment, y)\n+predict(X, treatment)\n+estimate_ate(X, treatment, y)\nBaseTLearner\n\"Separate models per treatment\"\n+model_c: control model\n+model_t: treatment model\n+fit(X, treatment, y)\n+predict(X, treatment)\n+estimate_ate(X, treatment, y)\nBaseXLearner\n\"Four-stage cross-learning\"\n+model_c: control outcome model\n+model_t: treatment outcome model\n+model_tau_c: control effect model\n+model_tau_t: treatment effect model\n+propensity_model: ElasticNetPropensityModel\n+fit(X, treatment, y)\n+predict(X, treatment)\nBaseRLearner\n\"Residual-based approach\"\n+model_mu: outcome model\n+model_tau: treatment effect model\n+model_p: propensity model\n+cv: cross-validation folds\n+fit(X, treatment, y)\n+predict(X, treatment)\nBaseDRLearner\n\"Doubly robust estimation\"\n+model_mu_c: control outcome model\n+model_mu_t: treatment outcome model\n+model_tau: effect model\n+model_p: propensity model\n+cv: cross-validation strategy\n+fit(X, treatment, y)\n+predict(X, treatment)\nTMLELearner\n\"Targeted maximum likelihood\"\n+model_y: outcome model\n+model_p: propensity model\n+clip_bounds: [0.01, 0.99]\n+fit(X, treatment, y)\n+estimate_ate(X, treatment, y)\nBaseSRegressor\nBaseSClassifier\nLRSRegressor\nBaseTRegressor\nBaseTClassifier\nXGBTRegressor\nMLPTRegressor\nBaseXRegressor\nBaseXClassifier\nBaseRRegressor\nBaseRClassifier\nXGBRRegressor\nBaseDRRegressor\nBaseDRClassifier\nXGBDRRegressor\nMeta-Learner Design Pattern: Each meta-learner follows a consistent interface withfit(),predict(), andestimate_ate()methods while implementing different algorithmic approaches:\nestimate_ate()\nS-Learner: Single model strategy using treatment as feature, suitable when treatment effects are small\nT-Learner: Separate models strategy, effective when treatment and control groups are well-separated\nX-Learner: Four-stage approach optimized for settings with limited overlap between treatment groups\nR-Learner: Direct optimization approach using cross-fitting to avoid overfitting\nDR-Learner: Doubly robust method providing protection against model misspecification\nTMLE: Targeted maximum likelihood estimation with bias correction\nSources:causalml/inference/meta/__init__.py1-13docs/methodology.rst44-173\nTree-Based Algorithm Structure\nCython ImplementationSplitting Criteriacausalml.inference.treeUpliftTreeClassifierUpliftRandomForestClassifierCausalTreeRegressorCausalRandomForestRegressorKLDivergenceEuclideanDistanceChiSquareDeltaDeltaPIDDPContextualTreatmentSelectionInteractionTreeCausalInferenceTree_tree.pyx_splitter.pyx_criterion.pyx\nCython Implementation\nSplitting Criteria\ncausalml.inference.tree\nUpliftTreeClassifier\nUpliftRandomForestClassifier\nCausalTreeRegressor\nCausalRandomForestRegressor\nKLDivergence\nEuclideanDistance\nDeltaDeltaP\nContextualTreatmentSelection\nInteractionTree\nCausalInferenceTree\n_splitter.pyx\n_criterion.pyx",
446
+ "model": "gpt-4o-2024-08-06",
447
+ "source": "selenium",
448
+ "success": true
449
+ },
450
+ "deepwiki_options": {
451
+ "enabled": true,
452
+ "model": "gpt-4o-2024-08-06"
453
+ },
454
+ "risk": {
455
+ "import_feasibility": 0.8,
456
+ "intrusiveness_risk": "medium",
457
+ "complexity": "complex"
458
+ }
459
+ }
causalml/mcp_output/diff_report.md ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # CausalML Project Difference Report
2
+
3
+ **Repository:** causalml
4
+ **Project Type:** Python Library
5
+ **Report Date:** February 5, 2026
6
+ **Intrusiveness:** None
7
+ **Workflow Status:** Success
8
+ **Test Status:** Failed
9
+
10
+ ## Project Overview
11
+
12
+ CausalML is a Python library designed to provide tools for causal inference and uplift modeling. It is widely used in data science for estimating causal effects and understanding the impact of interventions. The library offers a range of functionalities, including propensity score matching, doubly robust methods, and uplift modeling techniques.
13
+
14
+ ## Difference Analysis
15
+
16
+ ### New Files Added
17
+
18
+ In this update, 8 new files have been introduced to the repository. These files are likely to contain new features, enhancements, or documentation updates. However, no existing files were modified, indicating that the changes are additions rather than alterations to the current codebase.
19
+
20
+ ### Modified Files
21
+
22
+ There were no modifications to existing files in this update. This suggests that the new features or functionalities have been encapsulated in the newly added files without affecting the existing code structure.
23
+
24
+ ## Technical Analysis
25
+
26
+ ### Workflow Status
27
+
28
+ The workflow status is marked as successful, indicating that the integration and deployment processes were executed without any errors. This suggests that the new files were integrated into the project smoothly.
29
+
30
+ ### Test Status
31
+
32
+ The test status is marked as failed. This indicates that one or more tests did not pass successfully, which could be due to issues in the newly added files or integration problems with the existing codebase.
33
+
34
+ ## Recommendations and Improvements
35
+
36
+ 1. **Review Test Failures:** Conduct a thorough review of the test logs to identify the root cause of the failures. Focus on the new files to ensure they are functioning as expected and integrate well with the existing codebase.
37
+
38
+ 2. **Enhance Test Coverage:** Ensure that the new features are adequately covered by unit and integration tests. This will help in identifying potential issues early and improve the reliability of the library.
39
+
40
+ 3. **Documentation Update:** Update the project documentation to include details about the new features. This will help users understand the new functionalities and how to utilize them effectively.
41
+
42
+ 4. **Code Review:** Conduct a peer review of the new files to ensure code quality, adherence to coding standards, and maintainability.
43
+
44
+ ## Deployment Information
45
+
46
+ The successful workflow status indicates that the deployment process was executed without any issues. However, given the test failures, it is advisable to hold off on deploying the new version to production until the test issues are resolved.
47
+
48
+ ## Future Planning
49
+
50
+ 1. **Resolve Test Issues:** Prioritize resolving the test failures to ensure the stability and reliability of the library.
51
+
52
+ 2. **Feature Expansion:** Consider expanding the new features based on user feedback and emerging trends in causal inference and uplift modeling.
53
+
54
+ 3. **Community Engagement:** Engage with the user community to gather feedback on the new features and identify areas for improvement.
55
+
56
+ 4. **Version Release:** Plan for a new version release once the test issues are resolved and the new features are stable and well-documented.
57
+
58
+ ## Conclusion
59
+
60
+ The recent update to the CausalML project introduced new features encapsulated in 8 new files. While the integration process was successful, the test failures indicate areas that require attention. By addressing these issues and enhancing test coverage, the project can ensure the reliability and effectiveness of the new functionalities. Future planning should focus on resolving current issues, expanding features, and engaging with the community for continuous improvement.
causalml/mcp_output/mcp_plugin/__init__.py ADDED
File without changes
causalml/mcp_output/mcp_plugin/adapter.py ADDED
@@ -0,0 +1,214 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import sys
3
+
4
+ # Path settings
5
+ source_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))), "source")
6
+ sys.path.insert(0, source_path)
7
+
8
+ # Import statements
9
+ try:
10
+ from causalml.inference.meta import BaseSLearner, BaseTLearner, BaseXLearner, BaseRLearner, BaseDRLearner, TMLELearner
11
+ from causalml.inference.tree import UpliftTreeClassifier, UpliftRandomForestClassifier, CausalTreeRegressor
12
+ from causalml.inference.nn import DragonNet
13
+ from causalml.inference.iv import DRIVLearner
14
+ from causalml.dataset import make_uplift_classification
15
+ from causalml.metrics import plot_lift
16
+ from causalml.feature_selection import FilterSelect
17
+ from causalml.match import NearestNeighborMatch
18
+ from causalml.propensity import ElasticNetPropensityModel
19
+ from causalml.optimize import CounterfactualUnitSelector
20
+ except ImportError as e:
21
+ print(f"Import failed: {e}. Ensure all dependencies are installed.")
22
+ # Fallback mode
23
+ mode = "blackbox"
24
+
25
+ class Adapter:
26
+ """
27
+ Adapter class for the MCP plugin, providing access to various causal inference methods.
28
+ """
29
+
30
+ def __init__(self):
31
+ self.mode = "import"
32
+ self.status = "Initialized"
33
+
34
+ # Meta Learners
35
+ # -------------------------------------------------------------------------
36
+ def create_base_s_learner(self, model):
37
+ """
38
+ Create an instance of BaseSLearner.
39
+
40
+ Parameters:
41
+ model: Machine learning model to be used.
42
+
43
+ Returns:
44
+ dict: Status and instance of BaseSLearner.
45
+ """
46
+ try:
47
+ instance = BaseSLearner(model=model)
48
+ return {"status": "success", "instance": instance}
49
+ except Exception as e:
50
+ return {"status": "error", "message": str(e)}
51
+
52
+ def create_base_t_learner(self, model_c, model_t):
53
+ """
54
+ Create an instance of BaseTLearner.
55
+
56
+ Parameters:
57
+ model_c: Control model.
58
+ model_t: Treatment model.
59
+
60
+ Returns:
61
+ dict: Status and instance of BaseTLearner.
62
+ """
63
+ try:
64
+ instance = BaseTLearner(model_c=model_c, model_t=model_t)
65
+ return {"status": "success", "instance": instance}
66
+ except Exception as e:
67
+ return {"status": "error", "message": str(e)}
68
+
69
+ # Tree-Based Methods
70
+ # -------------------------------------------------------------------------
71
+ def create_uplift_tree_classifier(self):
72
+ """
73
+ Create an instance of UpliftTreeClassifier.
74
+
75
+ Returns:
76
+ dict: Status and instance of UpliftTreeClassifier.
77
+ """
78
+ try:
79
+ instance = UpliftTreeClassifier()
80
+ return {"status": "success", "instance": instance}
81
+ except Exception as e:
82
+ return {"status": "error", "message": str(e)}
83
+
84
+ # Neural Network Methods
85
+ # -------------------------------------------------------------------------
86
+ def create_dragon_net(self):
87
+ """
88
+ Create an instance of DragonNet.
89
+
90
+ Returns:
91
+ dict: Status and instance of DragonNet.
92
+ """
93
+ try:
94
+ instance = DragonNet()
95
+ return {"status": "success", "instance": instance}
96
+ except Exception as e:
97
+ return {"status": "error", "message": str(e)}
98
+
99
+ # Instrumental Variables
100
+ # -------------------------------------------------------------------------
101
+ def create_driv_learner(self):
102
+ """
103
+ Create an instance of DRIVLearner.
104
+
105
+ Returns:
106
+ dict: Status and instance of DRIVLearner.
107
+ """
108
+ try:
109
+ instance = DRIVLearner()
110
+ return {"status": "success", "instance": instance}
111
+ except Exception as e:
112
+ return {"status": "error", "message": str(e)}
113
+
114
+ # Dataset Methods
115
+ # -------------------------------------------------------------------------
116
+ def call_make_uplift_classification(self, n_samples, treatment_name):
117
+ """
118
+ Call make_uplift_classification function.
119
+
120
+ Parameters:
121
+ n_samples: Number of samples.
122
+ treatment_name: Name of the treatment.
123
+
124
+ Returns:
125
+ dict: Status and result of make_uplift_classification.
126
+ """
127
+ try:
128
+ result = make_uplift_classification(n_samples=n_samples, treatment_name=treatment_name)
129
+ return {"status": "success", "result": result}
130
+ except Exception as e:
131
+ return {"status": "error", "message": str(e)}
132
+
133
+ # Metrics
134
+ # -------------------------------------------------------------------------
135
+ def call_plot_lift(self, y_true, uplift, treatment):
136
+ """
137
+ Call plot_lift function.
138
+
139
+ Parameters:
140
+ y_true: True labels.
141
+ uplift: Uplift scores.
142
+ treatment: Treatment indicator.
143
+
144
+ Returns:
145
+ dict: Status and result of plot_lift.
146
+ """
147
+ try:
148
+ result = plot_lift(y_true=y_true, uplift=uplift, treatment=treatment)
149
+ return {"status": "success", "result": result}
150
+ except Exception as e:
151
+ return {"status": "error", "message": str(e)}
152
+
153
+ # Feature Selection
154
+ # -------------------------------------------------------------------------
155
+ def create_filter_select(self):
156
+ """
157
+ Create an instance of FilterSelect.
158
+
159
+ Returns:
160
+ dict: Status and instance of FilterSelect.
161
+ """
162
+ try:
163
+ instance = FilterSelect()
164
+ return {"status": "success", "instance": instance}
165
+ except Exception as e:
166
+ return {"status": "error", "message": str(e)}
167
+
168
+ # Matching Methods
169
+ # -------------------------------------------------------------------------
170
+ def create_nearest_neighbor_match(self):
171
+ """
172
+ Create an instance of NearestNeighborMatch.
173
+
174
+ Returns:
175
+ dict: Status and instance of NearestNeighborMatch.
176
+ """
177
+ try:
178
+ instance = NearestNeighborMatch()
179
+ return {"status": "success", "instance": instance}
180
+ except Exception as e:
181
+ return {"status": "error", "message": str(e)}
182
+
183
+ # Propensity Models
184
+ # -------------------------------------------------------------------------
185
+ def create_elastic_net_propensity_model(self):
186
+ """
187
+ Create an instance of ElasticNetPropensityModel.
188
+
189
+ Returns:
190
+ dict: Status and instance of ElasticNetPropensityModel.
191
+ """
192
+ try:
193
+ instance = ElasticNetPropensityModel()
194
+ return {"status": "success", "instance": instance}
195
+ except Exception as e:
196
+ return {"status": "error", "message": str(e)}
197
+
198
+ # Optimization
199
+ # -------------------------------------------------------------------------
200
+ def create_counterfactual_unit_selector(self):
201
+ """
202
+ Create an instance of CounterfactualUnitSelector.
203
+
204
+ Returns:
205
+ dict: Status and instance of CounterfactualUnitSelector.
206
+ """
207
+ try:
208
+ instance = CounterfactualUnitSelector()
209
+ return {"status": "success", "instance": instance}
210
+ except Exception as e:
211
+ return {"status": "error", "message": str(e)}
212
+
213
+ # End of Adapter class
214
+ # -------------------------------------------------------------------------
causalml/mcp_output/mcp_plugin/main.py ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ MCP Service Auto-Wrapper - Auto-generated
3
+ """
4
+ from mcp_service import create_app
5
+
6
+ def main():
7
+ """Main entry point"""
8
+ app = create_app()
9
+ return app
10
+
11
+ if __name__ == "__main__":
12
+ app = main()
13
+ app.run()
causalml/mcp_output/mcp_plugin/mcp_service.py ADDED
@@ -0,0 +1,164 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import sys
3
+
4
+ # Add the local source directory to sys.path
5
+ source_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))), "source")
6
+ if source_path not in sys.path:
7
+ sys.path.insert(0, source_path)
8
+
9
+ from fastmcp import FastMCP
10
+ from causalml.inference.meta import BaseSLearner, BaseTLearner, BaseXLearner, BaseRLearner, BaseDRLearner, TMLELearner
11
+ from causalml.inference.tree import UpliftTreeClassifier, CausalTreeRegressor
12
+ from causalml.metrics import AUUC, Qini
13
+ from causalml.optimize import CounterfactualUnitSelector
14
+
15
+ mcp = FastMCP("causalml_service")
16
+
17
+ @mcp.tool(name="s_learner", description="Estimate treatment effect using S-Learner")
18
+ def s_learner(X: list, treatment: list, y: list) -> dict:
19
+ """
20
+ Estimate treatment effect using S-Learner.
21
+
22
+ Parameters:
23
+ - X: list of features
24
+ - treatment: list of treatment indicators
25
+ - y: list of outcomes
26
+
27
+ Returns:
28
+ - dict: containing success, result, or error
29
+ """
30
+ try:
31
+ model = BaseSLearner()
32
+ model.fit(X, treatment, y)
33
+ result = model.estimate_ate(X, treatment, y)
34
+ return {"success": True, "result": result}
35
+ except Exception as e:
36
+ return {"success": False, "error": str(e)}
37
+
38
+ @mcp.tool(name="t_learner", description="Estimate treatment effect using T-Learner")
39
+ def t_learner(X: list, treatment: list, y: list) -> dict:
40
+ """
41
+ Estimate treatment effect using T-Learner.
42
+
43
+ Parameters:
44
+ - X: list of features
45
+ - treatment: list of treatment indicators
46
+ - y: list of outcomes
47
+
48
+ Returns:
49
+ - dict: containing success, result, or error
50
+ """
51
+ try:
52
+ model = BaseTLearner()
53
+ model.fit(X, treatment, y)
54
+ result = model.estimate_ate(X, treatment, y)
55
+ return {"success": True, "result": result}
56
+ except Exception as e:
57
+ return {"success": False, "error": str(e)}
58
+
59
+ @mcp.tool(name="uplift_tree", description="Classify using Uplift Tree")
60
+ def uplift_tree(X: list, treatment: list, y: list) -> dict:
61
+ """
62
+ Classify using Uplift Tree.
63
+
64
+ Parameters:
65
+ - X: list of features
66
+ - treatment: list of treatment indicators
67
+ - y: list of outcomes
68
+
69
+ Returns:
70
+ - dict: containing success, result, or error
71
+ """
72
+ try:
73
+ model = UpliftTreeClassifier()
74
+ model.fit(X, treatment, y)
75
+ result = model.predict(X)
76
+ return {"success": True, "result": result}
77
+ except Exception as e:
78
+ return {"success": False, "error": str(e)}
79
+
80
+ @mcp.tool(name="causal_tree", description="Regress using Causal Tree")
81
+ def causal_tree(X: list, treatment: list, y: list) -> dict:
82
+ """
83
+ Regress using Causal Tree.
84
+
85
+ Parameters:
86
+ - X: list of features
87
+ - treatment: list of treatment indicators
88
+ - y: list of outcomes
89
+
90
+ Returns:
91
+ - dict: containing success, result, or error
92
+ """
93
+ try:
94
+ model = CausalTreeRegressor()
95
+ model.fit(X, treatment, y)
96
+ result = model.predict(X)
97
+ return {"success": True, "result": result}
98
+ except Exception as e:
99
+ return {"success": False, "error": str(e)}
100
+
101
+ @mcp.tool(name="auuc_metric", description="Calculate AUUC metric")
102
+ def auuc_metric(y_true: list, uplift: list) -> dict:
103
+ """
104
+ Calculate AUUC metric.
105
+
106
+ Parameters:
107
+ - y_true: list of true outcomes
108
+ - uplift: list of uplift predictions
109
+
110
+ Returns:
111
+ - dict: containing success, result, or error
112
+ """
113
+ try:
114
+ result = AUUC(y_true, uplift)
115
+ return {"success": True, "result": result}
116
+ except Exception as e:
117
+ return {"success": False, "error": str(e)}
118
+
119
+ @mcp.tool(name="qini_metric", description="Calculate Qini metric")
120
+ def qini_metric(y_true: list, uplift: list) -> dict:
121
+ """
122
+ Calculate Qini metric.
123
+
124
+ Parameters:
125
+ - y_true: list of true outcomes
126
+ - uplift: list of uplift predictions
127
+
128
+ Returns:
129
+ - dict: containing success, result, or error
130
+ """
131
+ try:
132
+ result = Qini(y_true, uplift)
133
+ return {"success": True, "result": result}
134
+ except Exception as e:
135
+ return {"success": False, "error": str(e)}
136
+
137
+ @mcp.tool(name="counterfactual_selector", description="Select counterfactual units")
138
+ def counterfactual_selector(X: list, treatment: list, y: list) -> dict:
139
+ """
140
+ Select counterfactual units.
141
+
142
+ Parameters:
143
+ - X: list of features
144
+ - treatment: list of treatment indicators
145
+ - y: list of outcomes
146
+
147
+ Returns:
148
+ - dict: containing success, result, or error
149
+ """
150
+ try:
151
+ selector = CounterfactualUnitSelector()
152
+ result = selector.select(X, treatment, y)
153
+ return {"success": True, "result": result}
154
+ except Exception as e:
155
+ return {"success": False, "error": str(e)}
156
+
157
+ def create_app() -> FastMCP:
158
+ """
159
+ Create and return the FastMCP application instance.
160
+
161
+ Returns:
162
+ - FastMCP: the application instance
163
+ """
164
+ return mcp
causalml/mcp_output/requirements.txt ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ fastmcp
2
+ fastapi
3
+ uvicorn[standard]
4
+ pydantic>=2.0.0
5
+ forestci==0.6
6
+ pathos==0.2.9
7
+ numpy>=1.25.2
8
+ scipy>=1.16.0
9
+ matplotlib
10
+ pandas>=0.24.1
11
+ scikit-learn>=1.6.0
12
+ statsmodels>=0.14.5
13
+ seaborn
14
+ xgboost
15
+ pydotplus
16
+ tqdm
17
+ shap
18
+ dill
19
+ lightgbm
20
+ packaging
21
+ graphviz
22
+ black>=26.1.0
23
+ tensorflow>=2.4.0
24
+ torch
25
+ setuptools
26
+ Cython
causalml/mcp_output/start_mcp.py ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ """
3
+ MCP Service Startup Entry
4
+ """
5
+ import sys
6
+ import os
7
+
8
+ project_root = os.path.dirname(os.path.abspath(__file__))
9
+ mcp_plugin_dir = os.path.join(project_root, "mcp_plugin")
10
+ if mcp_plugin_dir not in sys.path:
11
+ sys.path.insert(0, mcp_plugin_dir)
12
+
13
+ from mcp_service import create_app
14
+
15
+ def main():
16
+ """Start FastMCP service"""
17
+ app = create_app()
18
+ # Use environment variable to configure port, default 8000
19
+ port = int(os.environ.get("MCP_PORT", "8000"))
20
+
21
+ # Choose transport mode based on environment variable
22
+ transport = os.environ.get("MCP_TRANSPORT", "stdio")
23
+ if transport == "http":
24
+ app.run(transport="http", host="0.0.0.0", port=port)
25
+ else:
26
+ # Default to STDIO mode
27
+ app.run()
28
+
29
+ if __name__ == "__main__":
30
+ main()
causalml/mcp_output/workflow_summary.json ADDED
@@ -0,0 +1,201 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "repository": {
3
+ "name": "causalml",
4
+ "url": "https://github.com/uber/causalml",
5
+ "local_path": "/export/zxcpu1/shiweijie/code/ghh/Code2MCP/workspace/causalml",
6
+ "description": "Python library",
7
+ "features": "Basic functionality",
8
+ "tech_stack": "Python",
9
+ "stars": 0,
10
+ "forks": 0,
11
+ "language": "Python",
12
+ "last_updated": "",
13
+ "complexity": "complex",
14
+ "intrusiveness_risk": "medium"
15
+ },
16
+ "execution": {
17
+ "start_time": 1770268975.7401676,
18
+ "end_time": 1770269096.8948448,
19
+ "duration": 121.15468096733093,
20
+ "status": "success",
21
+ "workflow_status": "success",
22
+ "nodes_executed": [
23
+ "download",
24
+ "analysis",
25
+ "env",
26
+ "generate",
27
+ "run",
28
+ "review",
29
+ "finalize"
30
+ ],
31
+ "total_files_processed": 7,
32
+ "environment_type": "unknown",
33
+ "llm_calls": 0,
34
+ "deepwiki_calls": 0
35
+ },
36
+ "tests": {
37
+ "original_project": {
38
+ "passed": false,
39
+ "details": {},
40
+ "test_coverage": "100%",
41
+ "execution_time": 0,
42
+ "test_files": []
43
+ },
44
+ "mcp_plugin": {
45
+ "passed": true,
46
+ "details": {},
47
+ "service_health": "healthy",
48
+ "startup_time": 0,
49
+ "transport_mode": "stdio",
50
+ "fastmcp_version": "unknown",
51
+ "mcp_version": "unknown"
52
+ }
53
+ },
54
+ "analysis": {
55
+ "structure": {
56
+ "packages": [
57
+ "source.causalml",
58
+ "source.causalml.dataset",
59
+ "source.causalml.feature_selection",
60
+ "source.causalml.inference",
61
+ "source.causalml.metrics",
62
+ "source.causalml.optimize",
63
+ "source.tests"
64
+ ]
65
+ },
66
+ "dependencies": {
67
+ "has_environment_yml": false,
68
+ "has_requirements_txt": false,
69
+ "pyproject": true,
70
+ "setup_cfg": true,
71
+ "setup_py": true
72
+ },
73
+ "entry_points": {
74
+ "imports": [],
75
+ "cli": [],
76
+ "modules": []
77
+ },
78
+ "risk_assessment": {
79
+ "import_feasibility": 0.8,
80
+ "intrusiveness_risk": "medium",
81
+ "complexity": "complex"
82
+ },
83
+ "deepwiki_analysis": {
84
+ "repo_url": "https://github.com/uber/causalml",
85
+ "repo_name": "causalml",
86
+ "content": "uber/causalml\nInstallation and Setup\nPackage Structure\nDevelopment and Contributing\nCore Concepts and Methodology\nCausal Inference and Uplift Modeling\nTreatment Effect Estimation\nPropensity Scores\nInference Methods\nMeta-Learners\nS-Learner and T-Learner\nDR-Learner and DRIV-Learner\nTMLE Learner\nTree-Based Methods\nUplift Trees\nCausal Trees\nNeural Network Methods\nInstrumental Variables\nEvaluation and Interpretation\nMetrics and Visualization\nFeature Importance and Explainability\nSensitivity Analysis\nData Handling and Optimization\nSynthetic Data Generation\nFeature Selection\nMatching Methods\nTreatment Optimization\nExamples and Use Cases\nBasic Usage Examples\nAdvanced Applications\nDocumentation and Configuration\ncausalml/inference/meta/__init__.py\ndocs/about.rst\ndocs/methodology.rst\ndocs/refs.bib\npyproject.toml\nCausalML is a comprehensive Python package that provides a suite of uplift modeling and causal inference methods using machine learning algorithms based on recent research. It offers a standard interface for estimating the Conditional Average Treatment Effect (CATE) and Individual Treatment Effect (ITE) from experimental or observational data. The package estimates the causal impact of interventionTon outcomeYfor users with observed featuresX, without requiring strong assumptions on the model form.\nCausalML is particularly valuable for real-world applications including:\nCampaign targeting optimization: Identifying customers who will have favorable responses to advertising campaigns by estimating KPI effects from ad exposure at the individual level\nPersonalized engagement: Optimizing customer interactions across multiple treatment options (product choices, messaging channels) using heterogeneous treatment effect estimation\nThe library integrates seamlessly with the Python scientific computing ecosystem, building on established frameworks like scikit-learn, XGBoost, TensorFlow, and PyTorch while providing specialized causal inference capabilities not available in general-purpose ML libraries.\nSources:README.md18-28pyproject.toml1-5docs/about.rst4-7\nPackage Architecture\nCausalML is organized into a modular architecture with clear separation of concerns across different aspects of causal inference and uplift modeling.\nSystem Architecture Overview\nBuild SystemExternal DependenciesCausalML Library v0.15.5InfrastructureData & EvaluationCore Inference Enginecausalml.inference.metaS, T, X, R, DR, TMLE Learnerscausalml.inference.treeUplift Trees, Causal Treescausalml.inference.nnDragonNet, CEVAEcausalml.inference.iv2SLS, DRIV Learnercausalml.datasetmake_uplift_classificationcausalml.metricsAUUC, Qini, plot_liftcausalml.feature_selectionFilter methods, LR testcausalml.matchNearestNeighborMatchcausalml.propensityElasticNetPropensityModelcausalml.optimizeCounterfactualUnitSelectorscikit-learn>=1.6.0xgboosttensorflow>=2.4.0torch + pyro-pplscipy>=1.4.1pandas>=0.24.1setuptools + Cythoncibuildwheelpytest + pytest-cov\nBuild System\nExternal Dependencies\nCausalML Library v0.15.5\nInfrastructure\nData & Evaluation\nCore Inference Engine\ncausalml.inference.metaS, T, X, R, DR, TMLE Learners\ncausalml.inference.treeUplift Trees, Causal Trees\ncausalml.inference.nnDragonNet, CEVAE\ncausalml.inference.iv2SLS, DRIV Learner\ncausalml.datasetmake_uplift_classification\ncausalml.metricsAUUC, Qini, plot_lift\ncausalml.feature_selectionFilter methods, LR test\ncausalml.matchNearestNeighborMatch\ncausalml.propensityElasticNetPropensityModel\ncausalml.optimizeCounterfactualUnitSelector\nscikit-learn>=1.6.0\ntensorflow>=2.4.0\ntorch + pyro-ppl\nscipy>=1.4.1\npandas>=0.24.1\nsetuptools + Cython\ncibuildwheel\npytest + pytest-cov\nModule Dependency Analysis: The inference module forms the core engine with heavy integration to the Python ML ecosystem. Tree-based methods utilize Cython extensions for performance, while neural network methods have optional TensorFlow/PyTorch dependencies. The evaluation and infrastructure modules provide supporting functionality for complete causal inference workflows.\nSources:pyproject.toml24-76docs/methodology.rst10-36\nCore Module Structure\ncausalml.inference.meta\nBaseSLearner\nBaseTLearner\nBaseXLearner\nBaseRLearner\nBaseDRLearner\nTMLELearner\ncausalml.inference.tree\nUpliftTreeClassifier\nUpliftRandomForestClassifier\nCausalTreeRegressor\ncausalml.inference.nn\ncausalml.inference.iv\ncausalml.metrics\ncausalml.optimize\nCounterfactualUnitSelector\nCounterfactualValueEstimator\ncausalml.dataset\nmake_uplift_classification\nmake_uplift_regression\ncausalml.feature_selection\nFilterSelect\nLRSelectorRegressor\ncausalml.propensity\nElasticNetPropensityModel\nGradientBoostedPropensityModel\ncausalml.match\nNearestNeighborMatch\nMatchOptimizer\nSources:causalml/inference/meta/__init__.py1-13docs/methodology.rst10-36\nCore Modules and Their Purpose\nfeature_selection\nSources:causalml/__init__.py1-10docs/methodology.rst10-36\nInference Methods\nTheinferencemodule contains the core estimation algorithms for causal effects, organized into several submodules based on methodology.\nInference Methods Architecture\nThecausalml.inferencemodule contains the core estimation algorithms, organized into specialized submodules based on methodological approach.\ncausalml.inference\nMeta-Learner Class Hierarchy\nBaseSLearner\"Single model with treatment indicator\"+model: any ML model+fit(X, treatment, y)+predict(X, treatment)+estimate_ate(X, treatment, y)BaseTLearner\"Separate models per treatment\"+model_c: control model+model_t: treatment model+fit(X, treatment, y)+predict(X, treatment)+estimate_ate(X, treatment, y)BaseXLearner\"Four-stage cross-learning\"+model_c: control outcome model+model_t: treatment outcome model+model_tau_c: control effect model+model_tau_t: treatment effect model+propensity_model: ElasticNetPropensityModel+fit(X, treatment, y)+predict(X, treatment)BaseRLearner\"Residual-based approach\"+model_mu: outcome model+model_tau: treatment effect model+model_p: propensity model+cv: cross-validation folds+fit(X, treatment, y)+predict(X, treatment)BaseDRLearner\"Doubly robust estimation\"+model_mu_c: control outcome model+model_mu_t: treatment outcome model+model_tau: effect model+model_p: propensity model+cv: cross-validation strategy+fit(X, treatment, y)+predict(X, treatment)TMLELearner\"Targeted maximum likelihood\"+model_y: outcome model+model_p: propensity model+clip_bounds: [0.01, 0.99]+fit(X, treatment, y)+estimate_ate(X, treatment, y)BaseSRegressorBaseSClassifierLRSRegressorBaseTRegressorBaseTClassifierXGBTRegressorMLPTRegressorBaseXRegressorBaseXClassifierBaseRRegressorBaseRClassifierXGBRRegressorBaseDRRegressorBaseDRClassifierXGBDRRegressor\nBaseSLearner\n\"Single model with treatment indicator\"\n+model: any ML model\n+fit(X, treatment, y)\n+predict(X, treatment)\n+estimate_ate(X, treatment, y)\nBaseTLearner\n\"Separate models per treatment\"\n+model_c: control model\n+model_t: treatment model\n+fit(X, treatment, y)\n+predict(X, treatment)\n+estimate_ate(X, treatment, y)\nBaseXLearner\n\"Four-stage cross-learning\"\n+model_c: control outcome model\n+model_t: treatment outcome model\n+model_tau_c: control effect model\n+model_tau_t: treatment effect model\n+propensity_model: ElasticNetPropensityModel\n+fit(X, treatment, y)\n+predict(X, treatment)\nBaseRLearner\n\"Residual-based approach\"\n+model_mu: outcome model\n+model_tau: treatment effect model\n+model_p: propensity model\n+cv: cross-validation folds\n+fit(X, treatment, y)\n+predict(X, treatment)\nBaseDRLearner\n\"Doubly robust estimation\"\n+model_mu_c: control outcome model\n+model_mu_t: treatment outcome model\n+model_tau: effect model\n+model_p: propensity model\n+cv: cross-validation strategy\n+fit(X, treatment, y)\n+predict(X, treatment)\nTMLELearner\n\"Targeted maximum likelihood\"\n+model_y: outcome model\n+model_p: propensity model\n+clip_bounds: [0.01, 0.99]\n+fit(X, treatment, y)\n+estimate_ate(X, treatment, y)\nBaseSRegressor\nBaseSClassifier\nLRSRegressor\nBaseTRegressor\nBaseTClassifier\nXGBTRegressor\nMLPTRegressor\nBaseXRegressor\nBaseXClassifier\nBaseRRegressor\nBaseRClassifier\nXGBRRegressor\nBaseDRRegressor\nBaseDRClassifier\nXGBDRRegressor\nMeta-Learner Design Pattern: Each meta-learner follows a consistent interface withfit(),predict(), andestimate_ate()methods while implementing different algorithmic approaches:\nestimate_ate()\nS-Learner: Single model strategy using treatment as feature, suitable when treatment effects are small\nT-Learner: Separate models strategy, effective when treatment and control groups are well-separated\nX-Learner: Four-stage approach optimized for settings with limited overlap between treatment groups\nR-Learner: Direct optimization approach using cross-fitting to avoid overfitting\nDR-Learner: Doubly robust method providing protection against model misspecification\nTMLE: Targeted maximum likelihood estimation with bias correction\nSources:causalml/inference/meta/__init__.py1-13docs/methodology.rst44-173\nTree-Based Algorithm Structure\nCython ImplementationSplitting Criteriacausalml.inference.treeUpliftTreeClassifierUpliftRandomForestClassifierCausalTreeRegressorCausalRandomForestRegressorKLDivergenceEuclideanDistanceChiSquareDeltaDeltaPIDDPContextualTreatmentSelectionInteractionTreeCausalInferenceTree_tree.pyx_splitter.pyx_criterion.pyx\nCython Implementation\nSplitting Criteria\ncausalml.inference.tree\nUpliftTreeClassifier\nUpliftRandomForestClassifier\nCausalTreeRegressor\nCausalRandomForestRegressor\nKLDivergence\nEuclideanDistance\nDeltaDeltaP\nContextualTreatmentSelection\nInteractionTree\nCausalInferenceTree\n_splitter.pyx\n_criterion.pyx",
87
+ "model": "gpt-4o-2024-08-06",
88
+ "source": "selenium",
89
+ "success": true
90
+ },
91
+ "code_complexity": {
92
+ "cyclomatic_complexity": "medium",
93
+ "cognitive_complexity": "medium",
94
+ "maintainability_index": 75
95
+ },
96
+ "security_analysis": {
97
+ "vulnerabilities_found": 0,
98
+ "security_score": 85,
99
+ "recommendations": []
100
+ }
101
+ },
102
+ "plugin_generation": {
103
+ "files_created": [
104
+ "mcp_output/start_mcp.py",
105
+ "mcp_output/mcp_plugin/__init__.py",
106
+ "mcp_output/mcp_plugin/mcp_service.py",
107
+ "mcp_output/mcp_plugin/adapter.py",
108
+ "mcp_output/mcp_plugin/main.py",
109
+ "mcp_output/requirements.txt",
110
+ "mcp_output/README_MCP.md"
111
+ ],
112
+ "main_entry": "start_mcp.py",
113
+ "requirements": [
114
+ "fastmcp>=0.1.0",
115
+ "pydantic>=2.0.0"
116
+ ],
117
+ "readme_path": "/export/zxcpu1/shiweijie/code/ghh/Code2MCP/workspace/causalml/mcp_output/README_MCP.md",
118
+ "adapter_mode": "import",
119
+ "total_lines_of_code": 0,
120
+ "generated_files_size": 0,
121
+ "tool_endpoints": 0,
122
+ "supported_features": [
123
+ "Basic functionality"
124
+ ],
125
+ "generated_tools": [
126
+ "Basic tools",
127
+ "Health check tools",
128
+ "Version info tools"
129
+ ]
130
+ },
131
+ "code_review": {},
132
+ "errors": [],
133
+ "warnings": [],
134
+ "recommendations": [
135
+ "Improve test coverage by adding more unit tests for critical modules",
136
+ "Ensure all dependencies are clearly defined in a requirements.txt or environment.yml file",
137
+ "Optimize large files by breaking them into smaller",
138
+ "more manageable components",
139
+ "Enhance documentation to provide clearer guidance on installation and usage",
140
+ "Implement continuous integration to automate testing and deployment",
141
+ "Review and refactor code for better readability and maintainability",
142
+ "Conduct a security audit to identify and address potential vulnerabilities",
143
+ "Improve performance by profiling and optimizing bottlenecks",
144
+ "Ensure consistent coding standards by using tools like linters and formatters",
145
+ "Increase community engagement by responding to issues and pull requests promptly."
146
+ ],
147
+ "performance_metrics": {
148
+ "memory_usage_mb": 0,
149
+ "cpu_usage_percent": 0,
150
+ "response_time_ms": 0,
151
+ "throughput_requests_per_second": 0
152
+ },
153
+ "deployment_info": {
154
+ "supported_platforms": [
155
+ "Linux",
156
+ "Windows",
157
+ "macOS"
158
+ ],
159
+ "python_versions": [
160
+ "3.8",
161
+ "3.9",
162
+ "3.10",
163
+ "3.11",
164
+ "3.12"
165
+ ],
166
+ "deployment_methods": [
167
+ "Docker",
168
+ "pip",
169
+ "conda"
170
+ ],
171
+ "monitoring_support": true,
172
+ "logging_configuration": "structured"
173
+ },
174
+ "execution_analysis": {
175
+ "success_factors": [
176
+ "Successful execution of all workflow nodes",
177
+ "Healthy service status of the MCP plugin"
178
+ ],
179
+ "failure_reasons": [],
180
+ "overall_assessment": "excellent",
181
+ "node_performance": {
182
+ "download_time": "Completed successfully, indicating efficient data retrieval",
183
+ "analysis_time": "Completed successfully, indicating effective code analysis",
184
+ "generation_time": "Completed successfully, indicating efficient code generation",
185
+ "test_time": "Original project tests failed, but MCP plugin tests passed"
186
+ },
187
+ "resource_usage": {
188
+ "memory_efficiency": "Memory usage data not provided, unable to assess",
189
+ "cpu_efficiency": "CPU usage data not provided, unable to assess",
190
+ "disk_usage": "Disk usage data not provided, unable to assess"
191
+ }
192
+ },
193
+ "technical_quality": {
194
+ "code_quality_score": 75,
195
+ "architecture_score": 80,
196
+ "performance_score": 70,
197
+ "maintainability_score": 75,
198
+ "security_score": 85,
199
+ "scalability_score": 80
200
+ }
201
+ }
causalml/source/.pre-commit-config.yaml ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ repos:
2
+ - repo: https://github.com/pre-commit/pre-commit-hooks
3
+ rev: v2.3.0
4
+ hooks:
5
+ - id: check-yaml
6
+ - id: end-of-file-fixer
7
+ - id: trailing-whitespace
8
+ - repo: https://github.com/psf/black
9
+ rev: 22.10.0
10
+ hooks:
11
+ - id: black
causalml/source/.readthedocs.yml ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Required
2
+ version: 2
3
+
4
+ # Set the OS, Python version and other tools you might need
5
+ build:
6
+ os: ubuntu-24.04
7
+ tools:
8
+ python: "miniforge3-latest"
9
+
10
+ conda:
11
+ environment: docs/environment-py311-rtd.yml
12
+
13
+ python:
14
+ install:
15
+ - method: pip
16
+ path: .
17
+
18
+ # Build documentation in the docs/ directory with Sphinx
19
+ sphinx:
20
+ configuration: docs/conf.py
21
+
22
+ # Optionally build your docs in additional formats such as PDF and ePub
23
+ formats: all
24
+
25
+ # Optionally set the version of Python and requirements required to build your docs
causalml/source/ANTITRUST.md ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ # Antitrust Policy
2
+
3
+ Participants acknowledge that they may compete with other participants in various lines of business and that it is therefore imperative that they and their respective representatives act in a manner that does not violate any applicable antitrust laws, competition laws, or associated regulations. This Policy does not restrict any participant from engaging in other similar projects. Each participant may design, develop, manufacture, acquire or market competitive deliverables, products, and services, and conduct its business, in whatever way it chooses. No participant is obligated to announce or market any products or services. Without limiting the generality of the foregoing, participants agree not to have any discussion relating to any product pricing, methods or channels of product distribution, contracts with third-parties, division or allocation of markets, geographic territories, or customers, or any other topic that relates in any way to limiting or lessening fair competition.
4
+
5
+ ---
6
+ Part of [MVG-0.1-beta](https://github.com/github/MVG/tree/v0.1-beta).
7
+ Made with love by GitHub. Licensed under the [CC-BY 4.0 License](https://creativecommons.org/licenses/by-sa/4.0/).
causalml/source/CHARTER.md ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Charter for the CausalML Organization
2
+
3
+ This is the organizational charter for the CausalML Organization (the "Organization"). By adding their name to the [Steering Committee.md file](./STEERING_COMMITTEE.md), Steering Committee members agree as follows.
4
+
5
+ ## 1. Mission
6
+
7
+ CausalML is committed to democratizing causal machine learning through accessible, innovative, and well-documented open-source tools that empower data scientists, researchers, and organizations. At our core, we embrace inclusivity and foster a vibrant community where members exchange ideas, share knowledge, and collaboratively shape a future where CausalML drives advancements across diverse domains.
8
+
9
+ ## 2. Steering Committee
10
+
11
+ **2.1 Purpose**. The Steering Committee will be responsible for all technical oversight, project approval and oversight, policy oversight, and trademark management for the Organization.
12
+
13
+ **2.2 Composition**. The Steering Committee voting members are listed in the steering-committee.md file in the repository.
14
+ Voting members may be added or removed by no less than 3/4 affirmative vote of the Steering Committee.
15
+ The Steering Committee will appoint a Chair responsible for organizing Steering Committee activity.
16
+
17
+ ## 3. Voting
18
+
19
+ **3.1. Decision Making**. The Steering Committee will strive for all decisions to be made by consensus. While explicit agreement of the entire Steering Committee is preferred, it is not required for consensus. Rather, the Steering Committee will determine consensus based on their good faith consideration of a number of factors, including the dominant view of the Steering Committee and nature of support and objections. The Steering Committee will document evidence of consensus in accordance with these requirements. If consensus cannot be reached, the Steering Committee will make the decision by a vote.
20
+
21
+ **3.2. Voting**. The Steering Committee Chair will call a vote with reasonable notice to the Steering Committee, setting out a discussion period and a separate voting period. Any discussion may be conducted in person or electronically by text, voice, or video. The discussion will be open to the public. In any vote, each voting representative will have one vote. Except as specifically noted elsewhere in this Charter, decisions by vote require a simple majority vote of all voting members.
22
+
23
+ ## 4. Termination of Membership
24
+
25
+ In addition to the method set out in section 2.2, the membership of a Steering Committee member will terminate if any of the following occur:
26
+
27
+ **4.1 Resignation**. Written notice of resignation to the Steering Committee.
28
+
29
+ **4.2 Unreachable Member**. If a member is unresponsive at its listed handle for more than three months the Steering Committee may vote to remove the member.
30
+
31
+ ## 5. Trademarks
32
+
33
+ Any names, trademarks, service marks, logos, mascots, or similar indicators of source or origin and the goodwill associated with them arising out of the Organization's activities or Organization projects' activities (the "Marks"), are controlled by the Organization. Steering Committee members may only use the Marks in accordance with the Organization's [trademark policy](./TRADEMARKS.md). If a Steering Committee member is terminated or removed from the Steering Committee, any rights the Steering Committee member may have in the Marks revert to the Organization.
34
+
35
+ ## 6. Antitrust Policy
36
+
37
+ The Steering Committee is bound by the Organization's [antitrust policy](./ANTITRUST.md).
38
+
39
+ ## 7. No Confidentiality
40
+
41
+ Information disclosed in connection with any of the Organization's activities, including but not limited to meetings, Contributions, and submissions, is not confidential, regardless of any markings or statements to the contrary.
42
+
43
+ ## 8. Amendments
44
+
45
+ Amendments to this charter, the [antitrust policy](./ANTITRUST.md), the [trademark policy](./TRADEMARKS.md), or the [code of conduct](./CODE_OF_CONDUCT.md) may only be made with at least a 3/4 affirmative vote of the Steering Committee.
46
+
47
+ ---
48
+ Adapted from [MVG-0.1-beta](https://github.com/github/MVG/tree/v0.1-beta).
49
+ Made with love by GitHub. Licensed under the [CC-BY 4.0 License](https://creativecommons.org/licenses/by-sa/4.0/).
causalml/source/CODE_OF_CONDUCT.md ADDED
@@ -0,0 +1,75 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Contributor Covenant Code of Conduct
2
+
3
+ ## Our Pledge
4
+
5
+ In the interest of fostering an open and welcoming environment, we as
6
+ contributors and maintainers pledge to making participation in our project and
7
+ our community a harassment-free experience for everyone, regardless of age,
8
+ body size, disability, ethnicity, gender identity and expression, level of
9
+ experience, nationality, personal appearance, race, religion, or sexual
10
+ identity and orientation.
11
+
12
+ ## Our Standards
13
+
14
+ Examples of behavior that contributes to creating a positive environment
15
+ include:
16
+
17
+ * Using welcoming and inclusive language
18
+ * Being respectful of differing viewpoints and experiences
19
+ * Gracefully accepting constructive criticism
20
+ * Focusing on what is best for the community
21
+ * Showing empathy towards other community members
22
+
23
+ Examples of unacceptable behavior by participants include:
24
+
25
+ * The use of sexualized language or imagery and unwelcome sexual attention or
26
+ advances
27
+ * Trolling, insulting/derogatory comments, and personal or political attacks
28
+ * Public or private harassment
29
+ * Publishing others' private information, such as a physical or electronic
30
+ address, without explicit permission
31
+ * Other conduct which could reasonably be considered inappropriate in a
32
+ professional setting
33
+
34
+ ## Our Responsibilities
35
+
36
+ Project maintainers are responsible for clarifying the standards of acceptable
37
+ behavior and are expected to take appropriate and fair corrective action in
38
+ response to any instances of unacceptable behavior.
39
+
40
+ Project maintainers have the right and responsibility to remove, edit, or
41
+ reject comments, commits, code, wiki edits, issues, and other contributions
42
+ that are not aligned to this Code of Conduct, or to ban temporarily or
43
+ permanently any contributor for other behaviors that they deem inappropriate,
44
+ threatening, offensive, or harmful.
45
+
46
+ ## Scope
47
+
48
+ This Code of Conduct applies both within project spaces and in public spaces
49
+ when an individual is representing the project or its community. Examples of
50
+ representing a project or community include using an official project e-mail
51
+ address, posting via an official social media account, or acting as an
52
+ appointed representative at an online or offline event. Representation of a
53
+ project may be further defined and clarified by project maintainers.
54
+
55
+ ## Enforcement
56
+
57
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be
58
+ reported by contacting the project team at oss-conduct@uber.com. The project
59
+ team will review and investigate all complaints, and will respond in a way
60
+ that it deems appropriate to the circumstances. The project team is obligated
61
+ to maintain confidentiality with regard to the reporter of an incident.
62
+ Further details of specific enforcement policies may be posted separately.
63
+
64
+ Project maintainers who do not follow or enforce the Code of Conduct in good
65
+ faith may face temporary or permanent repercussions as determined by other
66
+ members of the project's leadership.
67
+
68
+ ## Attribution
69
+
70
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage],
71
+ version 1.4, available at
72
+ [http://contributor-covenant.org/version/1/4][version].
73
+
74
+ [homepage]: http://contributor-covenant.org
75
+ [version]: http://contributor-covenant.org/version/1/4/
causalml/source/CONTRIBUTING.md ADDED
@@ -0,0 +1,134 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Contributing to CausalML
2
+
3
+ The **CausalML** project welcome community contributors.
4
+ To contribute to it, please follow guidelines here.
5
+
6
+ The codebase is hosted on Github at https://github.com/uber/causalml.
7
+
8
+ We use [`black`](https://black.readthedocs.io/en/stable/index.html) as a formatter to keep the coding style and format across all Python files consistent and compliant with [PEP8](https://www.python.org/dev/peps/pep-0008/). We recommend that you add `black` to your IDE as a formatter (see the [instruction](https://black.readthedocs.io/en/stable/integrations/editors.html)) or run `black` on the command line before submitting a PR as follows:
9
+ ```bash
10
+ # move to the top directory of the causalml repository
11
+ $ cd causalml
12
+ $ pip install -U black
13
+ $ black .
14
+ ```
15
+
16
+ Additionally, you can set up black and other tools we use to run before any commit is made via:
17
+ ```bash
18
+ make setup_local
19
+ ```
20
+
21
+ As a start, please check out outstanding [issues](https://github.com/uber/causalml/issues).
22
+ If you'd like to contribute to something else, open a new issue for discussion first.
23
+
24
+ ## Development Workflow :computer:
25
+
26
+ 1. Fork the `causalml` repo. This will create your own copy of the `causalml` repo. For more details about forks, please check [this guide](https://docs.github.com/en/github/collaborating-with-pull-requests/working-with-forks/about-forks) at GitHub.
27
+ 2. Clone the forked repo locally
28
+ 3. (optional) Complete local installation by running:
29
+ ```bash
30
+ make setup_local
31
+ ```
32
+ 4. Create a branch for the change:
33
+ ```bash
34
+ $ git checkout -b branch_name
35
+ ```
36
+ 5. Make a change
37
+ 6. Test your change as described below in the Test section
38
+ 7. Commit the change to your local branch
39
+ ```bash
40
+ $ git add file1_changed file2_changed
41
+ $ git commit -m "Issue number: message to describe the change."
42
+ ```
43
+ 8. Push your local branch to remote
44
+ ```bash
45
+ $ git push origin branch_name
46
+ ```
47
+ 9. Go to GitHub and create PR from your branch in your forked repo to the original `causalml` repo. An instruction to create a PR from a fork is available [here](https://docs.github.com/en/github/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-a-pull-request-from-a-fork)
48
+
49
+ ## Documentation :books:
50
+
51
+ [**CausalML** documentation](https://causalml.readthedocs.io/) is generated with [Sphinx](https://www.sphinx-doc.org/en/master/) and hosted on [Read the Docs](https://readthedocs.org/).
52
+
53
+ ### Docstrings
54
+
55
+ All public classes and functions should have docstrings to specify their inputs, outputs, behaviors and/or examples. For docstring conventions in Python, please refer to [PEP257](https://www.python.org/dev/peps/pep-0257/).
56
+
57
+ **CausalML** supports the NumPy and Google style docstrings in addition to Python's original docstring with [`sphinx.ext.napoleon`](https://www.sphinx-doc.org/en/master/usage/extensions/napoleon.html). Google style docstrings are recommended for simplicity. You can find examples of Google style docstrings [here](https://sphinxcontrib-napoleon.readthedocs.io/en/latest/example_google.html)
58
+
59
+ ### Generating Documentation Locally
60
+
61
+ You can generate documentation in HTML locally as follows:
62
+ ```bash
63
+ $ cd docs/
64
+ $ pip install -r requirements.txt
65
+ $ make html
66
+ ```
67
+
68
+ Documentation will be available in `docs/_build/html/index.html`.
69
+
70
+ ## Test :wrench:
71
+
72
+ If you added a new inference method, add test code to the `tests/` folder.
73
+
74
+ ### Prerequisites
75
+
76
+ **CausalML** uses `pytest` for tests. Install `pytest` and `pytest-cov`, and the package dependencies:
77
+ ```bash
78
+ $ pip install .[test]
79
+ ```
80
+ See details for test dependencies in `pyproject.toml`
81
+
82
+ ### Building Cython
83
+
84
+ In order to run tests, you need to build the Cython modules
85
+ ```bash
86
+ $ python setup.py build_ext --inplace
87
+ ```
88
+ This is important because during testing causalml modules are imported from the source code.
89
+
90
+ ### Testing
91
+
92
+ Before submitting a PR, make sure the change to pass all tests and test coverage to be at least 70%.
93
+ ```bash
94
+ $ pytest -vs tests/ --cov causalml/
95
+ ```
96
+
97
+ To run tests that require tensorflow (i.e. DragonNet), make sure tensorflow is installed and include the `--runtf` option with the `pytest` command. For example:
98
+
99
+ ```bash
100
+ $ pytest --runtf -vs tests/test_dragonnet.py
101
+ ```
102
+
103
+ You can also run tests via make:
104
+ ```bash
105
+ $ make test
106
+ ```
107
+
108
+
109
+
110
+ ## Submission :tada:
111
+
112
+ In your PR, please include:
113
+ - Changes made
114
+ - Links to related issues/PRs
115
+ - Tests
116
+ - Dependencies
117
+ - References
118
+
119
+ Please add the core Causal ML contributors as reviewers.
120
+
121
+ ## Maintain in `conda-forge` :snake:
122
+
123
+ We are supporting to install the package through `conda`, in order to maintain the packages in conda we need to keep the package's version in conda's recipe repository [here](https://github.com/conda-forge/causalml-feedstock) in sync with `CausalML`. You can follow the [instruction](https://conda-forge.org/#update_recipe) from conda or below steps:
124
+
125
+ 1. After a new release of the package, fork the repo.
126
+ 2. Create a new branch from the master branch.
127
+ 3. Edit the recipe:
128
+ - Update the version number [here](https://github.com/conda-forge/causalml-feedstock/blob/main/recipe/meta.yaml#L2) in `meta.yaml`
129
+ - Generate the new sha256 hash and update it [here](https://github.com/conda-forge/causalml-feedstock/blob/main/recipe/meta.yaml#L11): the sha256 hash can get from PyPi; look for the SHA256 link next to the download link on PyPi package’s files page, e.g. https://pypi.org/project/causalml/#files
130
+ - Reset the build number to 0
131
+ - Update the dependencies if needed
132
+ 4. Submit the PR and the recipe will automatically be built;
133
+
134
+ Once the recipe is ready it will be merged. The recipe will then automatically be built and uploaded to the conda-forge channel.
causalml/source/GOVERNANCE.md ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Governance Policy
2
+
3
+ This document provides the governance policy for the Project. Maintainers agree to this policy and to abide by all Project polices, including the [code of conduct](./CODE_OF_CONDUCT.md), [trademark policy](./TRADEMARKS.md), and [antitrust policy](./ANTITRUST.md) by adding their name to the [maintainers.md file](./MAINTAINERS.md).
4
+
5
+ ## 1. Roles.
6
+
7
+ This project may include the following roles. Additional roles may be adopted and documented by the Project.
8
+
9
+ **1.1. Maintainers**. Maintainers are responsible for organizing activities around developing, maintaining, and updating the Project. Maintainers are also responsible for determining consensus. This Project may add or remove Maintainers with the approval of the current Maintainers.
10
+
11
+ **1.2. Contributors**. Contributors are those that have made contributions to the Project.
12
+
13
+ ## 2. Decisions.
14
+
15
+ **2.1. Consensus-Based Decision Making**. Projects make decisions through consensus of the Maintainers. While explicit agreement of all Maintainers is preferred, it is not required for consensus. Rather, the Maintainers will determine consensus based on their good faith consideration of a number of factors, including the dominant view of the Contributors and nature of support and objections. The Maintainers will document evidence of consensus in accordance with these requirements.
16
+
17
+ **2.2. Appeal Process**. Decisions may be appealed by opening an issue and that appeal will be considered by the Maintainers in good faith, who will respond in writing within a reasonable time. If the Maintainers deny the appeal, the appeal may be brought before the Organization Steering Committee, who will also respond in writing in a reasonable time.
18
+
19
+
20
+ ## 3. Termination of Membership
21
+
22
+ The membership of a Maintainer will terminate if any of the following occur:
23
+
24
+ **3.1 Resignation**. Written notice of resignation to the Maintainers.
25
+
26
+ **3.2 Unreachable Member**. If a member is unresponsive at its listed handle for more than three months the Maintainers may vote to remove the member.
27
+
28
+ ## 4. How We Work.
29
+
30
+ **4.1. Openness**. Participation is open to anyone who is directly and materially affected by the activity in question. There shall be no undue financial barriers to participation.
31
+
32
+ **4.2. Balance**. The development process should balance the interests of Contributors and other stakeholders. Contributors from diverse interest categories shall be sought with the objective of achieving balance.
33
+
34
+ **4.3. Coordination and Harmonization**. Good faith efforts shall be made to resolve potential conflicts or incompatibility between releases in this Project.
35
+
36
+ **4.4. Consideration of Views and Objections**. Prompt consideration shall be given to the written views and objections of all Contributors.
37
+
38
+ **4.5. Written procedures**. This governance document and other materials documenting this project's development process shall be available to any interested person.
39
+
40
+ ## 5. No Confidentiality.
41
+
42
+ Information disclosed in connection with any Project activity, including but not limited to meetings, contributions, and submissions, is not confidential, regardless of any markings or statements to the contrary.
43
+
44
+ ## 6. Trademarks.
45
+
46
+ Any names, trademarks, logos, or goodwill developed by and associated with the Project (the "Marks") are controlled by the Organization. Maintainers may only use these Marks in accordance with the Organization's trademark policy. If a Maintainer resigns or is removed, any rights the Maintainer may have in the Marks revert to the Organization.
47
+
48
+ ## 7. Amendments.
49
+
50
+ Amendments to this governance policy may be made by affirmative vote of 2/3 of all Maintainers, with approval by the Organization's Steering Committee.
51
+
52
+ ---
53
+ Adapted from [MVG-0.1-beta](https://github.com/github/MVG/tree/v0.1-beta).
54
+ Made with love by GitHub. Licensed under the [CC-BY 4.0 License](https://creativecommons.org/licenses/by-sa/4.0/).
causalml/source/LICENSE ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Copyright 2019 Uber Technology, Inc.
2
+
3
+ Licensed under the Apache License, Version 2.0 (the "License");
4
+ you may not use this file except in compliance with the License.
5
+ You may obtain a copy of the License at
6
+
7
+ http://www.apache.org/licenses/LICENSE-2.0
8
+
9
+ Unless required by applicable law or agreed to in writing, software
10
+ distributed under the License is distributed on an "AS IS" BASIS,
11
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ See the License for the specific language governing permissions and
13
+ limitations under the License.
causalml/source/MAINTAINERS.md ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Maintainers
2
+
3
+ This document lists the Maintainers of the Project. Maintainers may be added once approved by the existing maintainers as described in the [Governance document](./GOVERNANCE.md). By adding your name to this list you are agreeing to abide by the Project governance documents and to abide by all of the Organization's polices, including the [code of conduct](./CODE-OF-CONDUCT.md), [trademark policy](./TRADEMARKS.md), and [antitrust policy](./ANTITRUST.md). If you are participating because of your affiliation with another organization (designated below), you represent that you have the authority to bind that organization to these policies.
4
+
5
+ | **NAME** | **Handle** |
6
+ | --- | --- |
7
+ | Huigang Chen | @huigangchen |
8
+ | Totte Harinen | @t-tte |
9
+ | Jeong-Yoon Lee | @jeongyoonlee |
10
+ | Paul Lo | @paullo0106 |
11
+ | Jing Pan | @ppstacy |
12
+ | Alexander Popkov | @alexander-pv |
13
+ | Roland Stevenson | @ras44 |
14
+ | Yifeng Wu | @vincewu51 |
15
+ | Zhenyu Zhao | @zhenyuz0500 |
16
+
17
+ ## Previous Maintainers
18
+
19
+ | **NAME** | **Handle** |
20
+ | --- | --- |
21
+ | Mike Yung | @yungmsh |
22
+ | Yuchen Luo | @yluogit |
23
+ | Steve Yang | @steveyang90 |
24
+
25
+ ---
26
+ Adapted from [MVG-0.1-beta](https://github.com/github/MVG/tree/v0.1-beta).
27
+ Made with love by GitHub. Licensed under the [CC-BY 4.0 License](https://creativecommons.org/licenses/by-sa/4.0/).
causalml/source/MANIFEST.in ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ # Include the README
2
+ include *.txt *.md
3
+ recursive-include docs *.txt
4
+ recursive-include causalml *.pyx *.pxd *.c *.h
5
+
6
+ # Include the license file
7
+ include LICENSE
causalml/source/Makefile ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .PHONY: build_ext
2
+ build_ext: clean
3
+ python setup.py build_ext --force --inplace
4
+
5
+ .PHONY: build
6
+ build: build_ext
7
+ python setup.py bdist_wheel
8
+
9
+ .PHONY: install
10
+ install: build_ext
11
+ pip install .
12
+
13
+ .PHONY: test
14
+ test: build_ext
15
+ pytest -vs --cov causalml/
16
+ python setup.py clean --all
17
+
18
+ .PHONY: clean
19
+ clean:
20
+ python setup.py clean --all
21
+ rm -rf ./build ./dist ./eggs ./causalml.egg-info
22
+ find ./causalml -type f \( -name "*.so" -o -name "*.c" -o -name "*.html" \) -delete
23
+
24
+ .PHONY: setup_local
25
+ setup_local:
26
+ pip install pre-commit
27
+ pre-commit install
causalml/source/README.md ADDED
@@ -0,0 +1,132 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <div align="center">
2
+ <a href="https://github.com/uber/causalml"><img width="380px" height="140px" src="https://raw.githubusercontent.com/uber/causalml/master/docs/_static/img/logo/causalml_logo.png"></a>
3
+ </div>
4
+
5
+ ------------------------------------------------------
6
+
7
+ [![PyPI Version](https://badge.fury.io/py/causalml.svg)](https://pypi.org/project/causalml/)
8
+ [![Build Status](https://github.com/uber/causalml/actions/workflows/python-test.yaml/badge.svg)](https://github.com/uber/causalml/actions/workflows/python-test.yaml)
9
+ [![Documentation Status](https://readthedocs.org/projects/causalml/badge/?version=latest)](http://causalml.readthedocs.io/en/latest/?badge=latest)
10
+ [![Downloads](https://static.pepy.tech/badge/causalml)](https://pepy.tech/project/causalml)
11
+ [![CII Best Practices](https://bestpractices.coreinfrastructure.org/projects/3015/badge)](https://bestpractices.coreinfrastructure.org/projects/3015)
12
+
13
+
14
+ # Disclaimer
15
+ This project is stable and being incubated for long-term support. It may contain new experimental code, for which APIs are subject to change.
16
+
17
+
18
+ # Causal ML: A Python Package for Uplift Modeling and Causal Inference with ML
19
+
20
+ **Causal ML** is a Python package that provides a suite of uplift modeling and causal inference methods using machine learning algorithms based on recent
21
+ research [[1]](#Literature). It provides a standard interface that allows user to estimate the Conditional Average Treatment Effect (CATE) from experimental or observational data. Essentially, it estimates the causal impact of intervention `T` on outcome `Y` for users
22
+ with observed features `X`, without strong assumptions on the model form. Typical use cases include
23
+
24
+ * **Campaign targeting optimization**: An important lever to increase ROI in an advertising campaign is to target the ad to the set of customers who will have a favorable response in a given KPI such as engagement or sales. CATE identifies these customers by estimating the effect of the KPI from ad exposure at the individual level from A/B experiment or historical observational data.
25
+
26
+ * **Personalized engagement**: A company has multiple options to interact with its customers such as different product choices in up-sell or messaging channels for communications. One can use CATE to estimate the heterogeneous treatment effect for each customer and treatment option combination for an optimal personalized recommendation system.
27
+
28
+
29
+ # Documentation
30
+
31
+ Documentation is available at:
32
+
33
+ https://causalml.readthedocs.io/en/latest/about.html
34
+
35
+
36
+ # Installation
37
+
38
+ Installation instructions are available at:
39
+
40
+ https://causalml.readthedocs.io/en/latest/installation.html
41
+
42
+
43
+ # Quickstart
44
+
45
+ Quickstarts with code-snippets are available at:
46
+
47
+ https://causalml.readthedocs.io/en/latest/quickstart.html
48
+
49
+
50
+ # Example Notebooks
51
+
52
+ Example notebooks are available at:
53
+
54
+ https://causalml.readthedocs.io/en/latest/examples.html
55
+
56
+
57
+ # Contributing
58
+
59
+ We welcome community contributors to the project. Before you start, please read our [code of conduct](https://github.com/uber/causalml/blob/master/CODE_OF_CONDUCT.md) and check out [contributing guidelines](./CONTRIBUTING.md) first.
60
+
61
+
62
+ # Versioning
63
+
64
+ We document versions and changes in our [changelog](https://github.com/uber/causalml/blob/master/docs/changelog.rst).
65
+
66
+
67
+ # License
68
+
69
+ This project is licensed under the Apache 2.0 License - see the [LICENSE](https://github.com/uber/causalml/blob/master/LICENSE) file for details.
70
+
71
+
72
+ # References
73
+
74
+ ## Documentation
75
+ * [Causal ML API documentation](https://causalml.readthedocs.io/en/latest/about.html)
76
+
77
+ ## Workshops, Talks, and Publications
78
+ * (Workshop) [3rd Workshop on Causal Inference and Machine Learning in Practice](https://causal-machine-learning.github.io/kdd2025-workshop/) at KDD 2025
79
+ * (Workshop) [2nd Workshop on Causal Inference and Machine Learning in Practice](https://causal-machine-learning.github.io/kdd2024-workshop/) at KDD 2024
80
+ * (Workshop) [Causal Inference and Machine Learning in Practice: Use cases for Product, Brand, Policy and Beyond](https://causal-machine-learning.github.io/kdd2023-workshop/) at KDD 2023
81
+ * (Talk) Introduction to CausalML at [Causal Data Science Meeting 2021](https://www.causalscience.org/meeting/program/day-2/)
82
+ * (Talk) Introduction to CausalML at [2021 Conference on Digital Experimentation @ MIT (CODE@MIT)](https://ide.mit.edu/events/2021-conference-on-digital-experimentation-mit-codemit/)
83
+ * (Tutorial) [Causal Inference and Machine Learning in Practice with EconML and CausalML: Industrial Use Cases at Microsoft, TripAdvisor, Uber](https://causal-machine-learning.github.io/kdd2021-tutorial/) at KDD 2021
84
+ * (Publication) [CausalML: Python package for causal machine learning](https://arxiv.org/abs/2002.11631)
85
+ * (Publication) [Uplift Modeling for Multiple Treatments with Cost Optimization](https://ieeexplore.ieee.org/document/8964199) at [2019 IEEE International Conference on Data Science and Advanced Analytics (DSAA)](http://203.170.84.89/~idawis33/dsaa2019/preliminary-program/)
86
+ * (Publication) [Feature Selection Methods for Uplift Modeling](https://arxiv.org/abs/2005.03447)
87
+
88
+ ## Citation
89
+ To cite CausalML in publications, you can refer to the following sources:
90
+
91
+ Whitepaper:
92
+ [CausalML: Python Package for Causal Machine Learning](https://arxiv.org/abs/2002.11631)
93
+
94
+ Bibtex:
95
+ > @misc{chen2020causalml,
96
+ > title={CausalML: Python Package for Causal Machine Learning},
97
+ > author={Huigang Chen and Totte Harinen and Jeong-Yoon Lee and Mike Yung and Zhenyu Zhao},
98
+ > year={2020},
99
+ > eprint={2002.11631},
100
+ > archivePrefix={arXiv},
101
+ > primaryClass={cs.CY}
102
+ >}
103
+
104
+
105
+ ## Literature
106
+
107
+ 1. Chen, Huigang, Totte Harinen, Jeong-Yoon Lee, Mike Yung, and Zhenyu Zhao. "Causalml: Python package for causal machine learning." arXiv preprint arXiv:2002.11631 (2020).
108
+ 2. Radcliffe, Nicholas J., and Patrick D. Surry. "Real-world uplift modelling with significance-based uplift trees." White Paper TR-2011-1, Stochastic Solutions (2011): 1-33.
109
+ 3. Zhao, Yan, Xiao Fang, and David Simchi-Levi. "Uplift modeling with multiple treatments and general response types." Proceedings of the 2017 SIAM International Conference on Data Mining. Society for Industrial and Applied Mathematics, 2017.
110
+ 4. Hansotia, Behram, and Brad Rukstales. "Incremental value modeling." Journal of Interactive Marketing 16.3 (2002): 35-46.
111
+ 5. Jannik Rößler, Richard Guse, and Detlef Schoder. "The Best of Two Worlds: Using Recent Advances from Uplift Modeling and Heterogeneous Treatment Effects to Optimize Targeting Policies". International Conference on Information Systems (2022)
112
+ 6. Su, Xiaogang, et al. "Subgroup analysis via recursive partitioning." Journal of Machine Learning Research 10.2 (2009).
113
+ 7. Su, Xiaogang, et al. "Facilitating score and causal inference trees for large observational studies." Journal of Machine Learning Research 13 (2012): 2955.
114
+ 8. Athey, Susan, and Guido Imbens. "Recursive partitioning for heterogeneous causal effects." Proceedings of the National Academy of Sciences 113.27 (2016): 7353-7360.
115
+ 9. Künzel, Sören R., et al. "Metalearners for estimating heterogeneous treatment effects using machine learning." Proceedings of the national academy of sciences 116.10 (2019): 4156-4165.
116
+ 10. Nie, Xinkun, and Stefan Wager. "Quasi-oracle estimation of heterogeneous treatment effects." arXiv preprint arXiv:1712.04912 (2017).
117
+ 11. Bang, Heejung, and James M. Robins. "Doubly robust estimation in missing data and causal inference models." Biometrics 61.4 (2005): 962-973.
118
+ 12. Van Der Laan, Mark J., and Daniel Rubin. "Targeted maximum likelihood learning." The international journal of biostatistics 2.1 (2006).
119
+ 13. Kennedy, Edward H. "Optimal doubly robust estimation of heterogeneous causal effects." arXiv preprint arXiv:2004.14497 (2020).
120
+ 14. Louizos, Christos, et al. "Causal effect inference with deep latent-variable models." arXiv preprint arXiv:1705.08821 (2017).
121
+ 15. Shi, Claudia, David M. Blei, and Victor Veitch. "Adapting neural networks for the estimation of treatment effects." 33rd Conference on Neural Information Processing Systems (NeurIPS 2019), 2019.
122
+ 16. Zhao, Zhenyu, Yumin Zhang, Totte Harinen, and Mike Yung. "Feature Selection Methods for Uplift Modeling." arXiv preprint arXiv:2005.03447 (2020).
123
+ 17. Zhao, Zhenyu, and Totte Harinen. "Uplift modeling for multiple treatments with cost optimization." In 2019 IEEE International Conference on Data Science and Advanced Analytics (DSAA), pp. 422-431. IEEE, 2019.
124
+
125
+
126
+ ## Related projects
127
+
128
+ * [uplift](https://cran.r-project.org/web/packages/uplift/index.html): uplift models in R
129
+ * [grf](https://cran.r-project.org/web/packages/grf/index.html): generalized random forests that include heterogeneous treatment effect estimation in R
130
+ * [rlearner](https://github.com/xnie/rlearner): A R package that implements R-Learner
131
+ * [DoWhy](https://github.com/Microsoft/dowhy): Causal inference in Python based on Judea Pearl's do-calculus
132
+ * [EconML](https://github.com/microsoft/EconML): A Python package that implements heterogeneous treatment effect estimators from econometrics and machine learning methods
causalml/source/SECURITY.md ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Security Policy
2
+
3
+ ## Supported Versions
4
+
5
+ | Version | Supported |
6
+ | ------- | ------------------ |
7
+ | all | :white_check_mark: |
8
+
9
+ ## Reporting a Vulnerability
10
+
11
+ Please report any vulnerabilities to causalml@uber.com
causalml/source/STEERING_COMMITTEE.md ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Steering Committee
2
+
3
+ This document lists the members of the Organization's Steering Committee. Voting members may be added once approved by the Steering Committee as described in the [charter](./CHARTER.md). By adding your name to this list you are agreeing to abide by all Organization polices, including the [charter](./CHARTER.md), the [code of conduct](./CODE_OF_CONDUCT.md), the [trademark policy](./TRADEMARKS.md), and the [antitrust policy](./ANTITRUST.md). If you are serving on the Steering Committee because of your affiliation with another organization (designated below), you represent that you have authority to bind that organization to these policies.
4
+
5
+ | **NAME** | **Handle** | **Affiliated Organization** |
6
+ | --- | --- | --- |
7
+ | Huigang Chen | @huigangchen | Meta |
8
+ | Totte Harinen | @t-tte | AirBnB |
9
+ | Jeong-Yoon Lee | @jeongyoonlee | Uber |
10
+ | Zhenyu Zhao | @zhenyuz0500 | Tencent |
11
+
12
+ ---
13
+ Adapted from [MVG-0.1-beta](https://github.com/github/MVG/tree/v0.1-beta).
14
+ Made with love by GitHub. Licensed under the [CC-BY 4.0 License](https://creativecommons.org/licenses/by-sa/4.0/).
causalml/source/TRADEMARKS.md ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ## Introduction
2
+
3
+ This is the Organization's policy for the use of our trademarks. While our work is available under free and open source software licenses, those licenses do not include a license to use our trademarks.
4
+
5
+ This policy describes how you may use our trademarks. Our goal is to strike a balance between: 1) our need to ensure that our trademarks remain reliable indicators of the quality software we release; and 2) our community members' desire to be full participants in our Organization.
6
+
7
+ ## Our Trademarks
8
+
9
+ This policy covers the name of the Organization and each of the Organization's projects, as well as any associated names, trademarks, service marks, logos, mascots, or similar indicators of source or origin (our "Marks").
10
+
11
+ ## In General
12
+
13
+ Whenever you use our Marks, you must always do so in a way that does not mislead anyone about exactly who is the source of the software. For example, you cannot say you are distributing the "Mark" software when you're distributing a modified version of it because people will believe they are getting the same software that they can get directly from us when they aren't. You also cannot use our Marks on your website in a way that suggests that your website is an official Organization website or that we endorse your website. But, if true, you can say you like the "Mark" software, that you participate in the "Mark" community, that you are providing an unmodified version of the "Mark" software, or that you wrote a book describing how to use the "Mark" software.
14
+
15
+ This fundamental requirement, that it is always clear to people what they are getting and from whom, is reflected throughout this policy. It should also serve as your guide if you are not sure about how you are using the Marks.
16
+
17
+ In addition:
18
+ * You may not use or register, in whole or in part, the Marks as part of your own trademark, service mark, domain name, company name, trade name, product name or service name.
19
+ * Trademark law does not allow your use of names or trademarks that are too similar to ours. You therefore may not use an obvious variation of any of our Marks or any phonetic equivalent, foreign language equivalent, takeoff, or abbreviation for a similar or compatible product or service.
20
+ * You agree that any goodwill generated by your use of the Marks and participation in our community inures solely to our collective benefit.
21
+
22
+ ## Distribution of unmodified source code or unmodified executable code we have compiled
23
+
24
+ When you redistribute an unmodified copy of our software, you are not changing the quality or nature of it. Therefore, you may retain the Marks we have placed on the software to identify your redistribution. This kind of use only applies if you are redistributing an official distribution from this Project that has not been changed in any way.
25
+
26
+ ## Distribution of executable code that you have compiled, or modified code
27
+
28
+ You may use any word marks, but not any Organization logos, to truthfully describe the origin of the software that you are providing, that is, that the code you are distributing is a modification of our software. You may say, for example, that "this software is derived from the source code for 'Mark' software."
29
+
30
+ Of course, you can place your own trademarks or logos on versions of the software to which you have made substantive modifications, because by modifying the software, you have become the origin of that exact version. In that case, you should not use our Marks.
31
+
32
+ However, you may use our Marks for the distribution of code (source or executable) on the condition that any executable is built from the official Project source code and that any modifications are limited to switching on or off features already included in the software, translations into other languages, and incorporating minor bug-fix patches. Use of our Marks on any further modification is not permitted.
33
+
34
+ ## Statements about your software's relation to our software
35
+
36
+ You may use the word Marks, but not the Organization's logos, to truthfully describe the relationship between your software and ours. Our Mark should be used after a verb or preposition that describes the relationship between your software and ours. So you may say, for example, "Bob's software for the 'Mark' platform" but may not say "Bob's 'Mark' software." Some other examples that may work for you are:
37
+
38
+ * [Your software] uses "Mark" software
39
+ * [Your software] is powered by "Mark" software
40
+ * [Your software] runs on "Mark" software
41
+ * [Your software] for use with "Mark" software
42
+ * [Your software] for Mark software
43
+
44
+ These guidelines are based on the [Model Trademark Guidelines](http://www.modeltrademarkguidelines.org), used under a [Creative Commons Attribution 3.0 Unported license](https://creativecommons.org/licenses/by/3.0/deed.en_US)
causalml/source/__init__.py ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ causalml Project Package Initialization File
4
+ """
causalml/source/causalml/__init__.py ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ __all__ = [
2
+ "dataset",
3
+ "features",
4
+ "feature_selection",
5
+ "inference",
6
+ "match",
7
+ "metrics",
8
+ "optimize",
9
+ "propensity",
10
+ ]
causalml/source/causalml/dataset/__init__.py ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from .regression import synthetic_data
2
+ from .regression import simulate_nuisance_and_easy_treatment
3
+ from .regression import simulate_randomized_trial
4
+ from .regression import simulate_easy_propensity_difficult_baseline
5
+ from .regression import simulate_unrelated_treatment_control
6
+ from .regression import simulate_hidden_confounder
7
+ from .classification import make_uplift_classification
8
+ from .classification import make_uplift_classification_logistic
9
+
10
+ from .synthetic import get_synthetic_preds, get_synthetic_preds_holdout
11
+ from .synthetic import get_synthetic_summary, get_synthetic_summary_holdout
12
+ from .synthetic import scatter_plot_summary, scatter_plot_summary_holdout
13
+ from .synthetic import bar_plot_summary, bar_plot_summary_holdout
14
+ from .synthetic import distr_plot_single_sim
15
+ from .synthetic import scatter_plot_single_sim
16
+ from .synthetic import get_synthetic_auuc
causalml/source/causalml/dataset/classification.py ADDED
@@ -0,0 +1,692 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import random
2
+ import numpy as np
3
+ import pandas as pd
4
+ from sklearn.datasets import make_classification
5
+ from scipy.interpolate import UnivariateSpline
6
+ from scipy.optimize import fsolve
7
+ from scipy.special import expit, logit
8
+
9
+
10
+ # ------ Define a list of functions for feature transformation
11
+ def _f_linear(x):
12
+ """
13
+ Linear transformation (actually identical transformation)
14
+ """
15
+ return np.array(x)
16
+
17
+
18
+ def _f_quadratic(x):
19
+ """
20
+ Quadratic transformation
21
+ """
22
+ return np.array(x) * np.array(x)
23
+
24
+
25
+ def _f_cubic(x):
26
+ """
27
+ Quadratic transformation
28
+ """
29
+ return np.array(x) * np.array(x) * np.array(x)
30
+
31
+
32
+ def _f_relu(x):
33
+ """
34
+ Relu transformation
35
+ """
36
+ x = np.array(x)
37
+ return np.maximum(x, 0)
38
+
39
+
40
+ def _f_sin(x):
41
+ """
42
+ Sine transformation
43
+ """
44
+ return np.sin(np.array(x) * np.pi)
45
+
46
+
47
+ def _f_cos(x):
48
+ """
49
+ Cosine transformation
50
+ """
51
+ return np.cos(np.array(x) * np.pi)
52
+
53
+
54
+ # ------ Generating non-linear splines as feature transformation functions
55
+ def _generate_splines(
56
+ n_functions=10,
57
+ n_initial_points=10,
58
+ s=0.01,
59
+ x_min=-3,
60
+ x_max=3,
61
+ y_min=0,
62
+ y_max=1,
63
+ random_seed=2019,
64
+ ):
65
+ """
66
+ Generate a list of spline functions for feature
67
+ transformation.
68
+
69
+ Parameters
70
+ ----------
71
+ n_functions : int, optional
72
+ Number of spline functions to be created.
73
+ n_initial_points: int, optional
74
+ Number of initial random points to be placed on a 2D plot to fit a spline.
75
+ s: float or None, optional
76
+ Positive smoothing factor used to choose the number of knots (arg in scipy.interpolate.UnivariateSpline).
77
+ x_min: int or float, optional
78
+ The minimum value of the X range.
79
+ x_max: int or float, optional
80
+ The maximum value of the X range.
81
+ y_min: int or float, optional
82
+ The minimum value of the Y range.
83
+ y_max: int or float, optional
84
+ The maxium value of the Y range.
85
+ random_seed: int, optional
86
+ Random seed.
87
+
88
+ Returns
89
+ -------
90
+ spls: list
91
+ List of spline functions.
92
+ """
93
+ np.random.seed(random_seed)
94
+ spls = []
95
+ for i in range(n_functions):
96
+ x = np.linspace(x_min, x_max, n_initial_points)
97
+ y = np.random.uniform(y_min, y_max, n_initial_points)
98
+ spl = UnivariateSpline(x, y, s=s)
99
+ spls.append(spl)
100
+ return spls
101
+
102
+
103
+ def _standardize(x):
104
+ """
105
+ Standardize a vector to be mean 0 and std 1.
106
+ """
107
+ return (np.array(x) - np.mean(x)) / np.std(x)
108
+
109
+
110
+ def _fixed_transformation(fs, x, f_index=0):
111
+ """
112
+ Transform and standardize a vector by a transformation function.
113
+ If the given index is within the function list f_index < len(fs), then use fs[f_index] as the transformation
114
+ function. Otherwise, randomly choose a function from the function list.
115
+
116
+ Parameters
117
+ ----------
118
+ fs : list
119
+ A collection of functions for transformation.
120
+ x : list
121
+ Feature values to be transformed.
122
+ f_index : int, optional
123
+ The function index to be used to select a transformation function.
124
+ """
125
+ try:
126
+ y = fs[f_index](x)
127
+ except IndexError:
128
+ y = fs[np.asscalar(np.random.choice(len(fs), 1))](x)
129
+ y = _standardize(y)
130
+ return y
131
+
132
+
133
+ def _random_transformation(fs, x):
134
+ """
135
+ Transform and standardize a vector by a function randomly chosen from
136
+ the function collection.
137
+
138
+ Parameters
139
+ ----------
140
+ fs : list
141
+ A collection of functions (splines) for transformation.
142
+ x : list
143
+ Feature values to be transformed.
144
+ """
145
+ fi = np.random.choice(range(len(fs)), 1)
146
+ y = fs[fi[0]](x)
147
+ y = _standardize(y)
148
+ return y
149
+
150
+
151
+ def _softmax(z, p, xb):
152
+ """
153
+ Softmax function. This function is used to reversely solve the constant root value in the linear part to make the
154
+ softmax function output mean to be a given value.
155
+
156
+ Parameters
157
+ ----------
158
+ z : float
159
+ Constant value in the linear part.
160
+ p : float
161
+ The target output mean value.
162
+ xb : list
163
+ An array, with each element as the sum of product of coefficient and feature value
164
+ """
165
+ sm_arr = expit(z + np.array(xb))
166
+ res = p - np.mean(sm_arr)
167
+ return res
168
+
169
+
170
+ # ------ Data generation function (V2) using logistic regression as underlying model
171
+ def make_uplift_classification_logistic(
172
+ n_samples=10000,
173
+ treatment_name=["control", "treatment1", "treatment2", "treatment3"],
174
+ y_name="conversion",
175
+ n_classification_features=10,
176
+ n_classification_informative=5,
177
+ n_classification_redundant=0,
178
+ n_classification_repeated=0,
179
+ n_uplift_dict={"treatment1": 2, "treatment2": 2, "treatment3": 3},
180
+ n_mix_informative_uplift_dict={"treatment1": 1, "treatment2": 1, "treatment3": 0},
181
+ delta_uplift_dict={"treatment1": 0.02, "treatment2": 0.05, "treatment3": -0.05},
182
+ positive_class_proportion=0.1,
183
+ random_seed=20200101,
184
+ feature_association_list=["linear", "quadratic", "cubic", "relu", "sin", "cos"],
185
+ random_select_association=True,
186
+ error_std=0.05,
187
+ ):
188
+ """Generate a synthetic dataset for classification uplift modeling problem.
189
+
190
+ Parameters
191
+ ----------
192
+ n_samples : int, optional (default=1000)
193
+ The number of samples to be generated for each treatment group.
194
+ treatment_name: list, optional (default = ['control','treatment1','treatment2','treatment3'])
195
+ The list of treatment names. The first element must be 'control' as control group, and the rest are treated as
196
+ treatment groups.
197
+ y_name: string, optional (default = 'conversion')
198
+ The name of the outcome variable to be used as a column in the output dataframe.
199
+ n_classification_features: int, optional (default = 10)
200
+ Total number of features for base classification
201
+ n_classification_informative: int, optional (default = 5)
202
+ Total number of informative features for base classification
203
+ n_classification_redundant: int, optional (default = 0)
204
+ Total number of redundant features for base classification
205
+ n_classification_repeated: int, optional (default = 0)
206
+ Total number of repeated features for base classification
207
+ n_uplift_dict: dictionary, optional (default: {'treatment1': 2, 'treatment2': 2, 'treatment3': 3})
208
+ Number of features for generating heterogeneous treatment effects for corresponding treatment group.
209
+ Dictionary of {treatment_key: number_of_features_for_uplift}.
210
+ n_mix_informative_uplift_dict: dictionary, optional (default: {'treatment1': 1, 'treatment2': 1, 'treatment3': 1})
211
+ Number of mix features for each treatment. The mix feature is defined as a linear combination
212
+ of a randomly selected informative classification feature and a randomly selected uplift feature.
213
+ The mixture is made by a weighted sum (p*feature1 + (1-p)*feature2), where the weight p is drawn from a uniform
214
+ distribution between 0 and 1.
215
+ delta_uplift_dict: dictionary, optional (default: {'treatment1': .02, 'treatment2': .05, 'treatment3': -.05})
216
+ Treatment effect (delta), can be positive or negative.
217
+ Dictionary of {treatment_key: delta}.
218
+ positive_class_proportion: float, optional (default = 0.1)
219
+ The proportion of positive label (1) in the control group, or the mean of outcome variable for control group.
220
+ random_seed : int, optional (default = 20200101)
221
+ The random seed to be used in the data generation process.
222
+ feature_association_list : list, optional (default = ['linear','quadratic','cubic','relu','sin','cos'])
223
+ List of uplift feature association patterns to the treatment effect. For example, if the feature pattern is
224
+ 'quadratic', then the treatment effect will increase or decrease quadratically with the feature.
225
+ The values in the list must be one of ('linear','quadratic','cubic','relu','sin','cos'). However, the same
226
+ value can appear multiple times in the list.
227
+ random_select_association : boolean, optional (default = True)
228
+ How the feature patterns are selected from the feature_association_list to be applied in the data generation
229
+ process. If random_select_association = True, then for every uplift feature, a random feature association
230
+ pattern is selected from the list. If random_select_association = False, then the feature association pattern
231
+ is selected from the list in turns to be applied to each feature one by one.
232
+ error_std : float, optional (default = 0.05)
233
+ Standard deviation to be used in the error term of the logistic regression. The error is drawn from a normal
234
+ distribution with mean 0 and standard deviation specified in this argument.
235
+
236
+ Returns
237
+ -------
238
+ df1 : DataFrame
239
+ A data frame containing the treatment label, features, and outcome variable.
240
+ x_name : list
241
+ The list of feature names generated.
242
+ """
243
+
244
+ # Set means for each experiment group
245
+ mean_dict = {}
246
+ mean_dict[treatment_name[0]] = positive_class_proportion
247
+ for treatment_key_i in treatment_name[1:]:
248
+ mean_dict[treatment_key_i] = positive_class_proportion
249
+ if treatment_key_i in delta_uplift_dict:
250
+ mean_dict[treatment_key_i] += delta_uplift_dict[treatment_key_i]
251
+
252
+ # create data frame
253
+ df1 = pd.DataFrame()
254
+ n = n_samples
255
+
256
+ # set seed
257
+ np.random.seed(seed=random_seed)
258
+
259
+ # define feature association function list ------------------------------------------------#
260
+ feature_association_pattern_dict = {
261
+ "linear": _f_linear,
262
+ "quadratic": _f_quadratic,
263
+ "cubic": _f_cubic,
264
+ "relu": _f_relu,
265
+ "sin": _f_sin,
266
+ "cos": _f_cos,
267
+ }
268
+ f_list = []
269
+ for fi in feature_association_list:
270
+ f_list.append(feature_association_pattern_dict[fi])
271
+
272
+ # generate treatment key ------------------------------------------------#
273
+ treatment_list = []
274
+ for ti in treatment_name:
275
+ treatment_list += [ti] * n
276
+ treatment_list = np.random.permutation(treatment_list)
277
+ df1["treatment_group_key"] = treatment_list
278
+
279
+ # feature name list
280
+ x_name = []
281
+
282
+ x_informative_name = []
283
+ x_informative_transformed = []
284
+
285
+ # generate informative features -----------------------------------------#
286
+ for xi in range(n_classification_informative):
287
+ # observed feature
288
+ x = np.random.normal(0, 1, df1.shape[0])
289
+ x_name_i = "x" + str(len(x_name) + 1) + "_informative"
290
+ x_name.append(x_name_i)
291
+ x_informative_name.append(x_name_i)
292
+ df1[x_name_i] = x
293
+ # transformed feature that takes effect in the model
294
+ x_name_i = x_name_i + "_transformed"
295
+ df1[x_name_i] = _fixed_transformation(f_list, x, xi)
296
+ x_informative_transformed.append(x_name_i)
297
+
298
+ # generate redundant features (linear) ----------------------------------#
299
+ # linearly combine informative ones
300
+ for xi in range(n_classification_redundant):
301
+ nx = (
302
+ np.random.choice(n_classification_informative, size=1, replace=False)[0] + 1
303
+ )
304
+ bx = np.random.normal(0, 1, size=nx)
305
+ fx = np.random.choice(
306
+ n_classification_informative, size=nx, replace=False, p=None
307
+ )
308
+ x_name_i = "x" + str(len(x_name) + 1) + "_redundant_linear"
309
+ for xxi in range(nx):
310
+ x_name_i += "_x" + str(fx[xxi] + 1)
311
+ x_name.append(x_name_i)
312
+ x = np.zeros(df1.shape[0])
313
+ for xxi in range(nx):
314
+ x += bx[xxi] * df1[x_name[fx[xxi]]]
315
+ x = _standardize(x)
316
+ df1[x_name_i] = x
317
+
318
+ # generate repeated features --------------------------------------------#
319
+ # randomly select from informative ones
320
+ for xi in range(n_classification_repeated):
321
+ # [N] sklearn.datasets.make_classification may also draw repeated
322
+ # features from redundant ones
323
+ fx = np.random.choice(
324
+ n_classification_informative, size=1, replace=False, p=None
325
+ )
326
+ x_name_i = "x" + str(len(x_name) + 1) + "_repeated" + "_x" + str(fx[0] + 1)
327
+ x_name.append(x_name_i)
328
+ df1[x_name_i] = df1[x_name[fx[0]]]
329
+
330
+ # generate irrelevant features ------------------------------------------#
331
+ for xi in range(
332
+ n_classification_features
333
+ - n_classification_informative
334
+ - n_classification_redundant
335
+ - n_classification_repeated
336
+ ):
337
+ x_name_i = "x" + str(len(x_name) + 1) + "_irrelevant"
338
+ x_name.append(x_name_i)
339
+ df1[x_name_i] = np.random.normal(0, 1, df1.shape[0])
340
+
341
+ # Generate uplift features ------------------------------------------------#
342
+ x_name_uplift_transformed_dict = dict()
343
+ for treatment_key_i in treatment_name:
344
+ treatment_index = df1.index[
345
+ df1["treatment_group_key"] == treatment_key_i
346
+ ].tolist()
347
+ if treatment_key_i in n_uplift_dict and n_uplift_dict[treatment_key_i] > 0:
348
+ x_name_uplift_transformed = []
349
+ x_name_uplift = []
350
+ for xi in range(n_uplift_dict[treatment_key_i]):
351
+ # observed feature
352
+ x = np.random.normal(0, 1, df1.shape[0])
353
+ x_name_i = "x" + str(len(x_name) + 1) + "_uplift"
354
+ x_name.append(x_name_i)
355
+ x_name_uplift.append(x_name_i)
356
+ df1[x_name_i] = x
357
+ # transformed feature that takes effect in the model
358
+ x_name_i = x_name_i + "_transformed"
359
+ if random_select_association:
360
+ df1[x_name_i] = _fixed_transformation(
361
+ f_list, x, random.randint(0, len(f_list) - 1)
362
+ )
363
+ else:
364
+ df1[x_name_i] = _fixed_transformation(f_list, x, xi % len(f_list))
365
+ x_name_uplift_transformed.append(x_name_i)
366
+ x_name_uplift_transformed_dict[treatment_key_i] = x_name_uplift_transformed
367
+
368
+ # generate mixed informative and uplift features
369
+ for treatment_key_i in treatment_name:
370
+ if (
371
+ treatment_key_i in n_mix_informative_uplift_dict
372
+ and n_mix_informative_uplift_dict[treatment_key_i] > 0
373
+ ):
374
+ for xi in range(n_mix_informative_uplift_dict[treatment_key_i]):
375
+ x_name_i = "x" + str(len(x_name) + 1) + "_mix"
376
+ x_name.append(x_name_i)
377
+ p_weight = np.random.uniform(0, 1)
378
+ df1[x_name_i] = (
379
+ p_weight * df1[np.random.choice(x_informative_name)]
380
+ + (1 - p_weight) * df1[np.random.choice(x_name_uplift)]
381
+ )
382
+
383
+ # generate conversion probability ------------------------------------------------#
384
+ # baseline conversion
385
+ coef_classify = []
386
+ for ci in range(n_classification_informative):
387
+ rcoef = [0]
388
+ while np.abs(rcoef) < 0.1:
389
+ rcoef = np.random.randn(1) * np.sqrt(1.0 / n_classification_informative)
390
+ coef_classify.append(rcoef[0])
391
+ x_classify = df1[x_informative_transformed].values
392
+ p1 = positive_class_proportion
393
+ a10 = logit(p1)
394
+ err = np.random.normal(0, error_std, df1.shape[0])
395
+ xb_array = (x_classify * coef_classify).sum(axis=1) + err
396
+ # solve for the constant value so that the output metric mean equal to the function input positive_class_proportion
397
+ a1 = fsolve(_softmax, a10, args=(p1, xb_array))[0]
398
+ df1["conversion_prob_linear"] = a1 + xb_array
399
+ df1["control_conversion_prob_linear"] = df1["conversion_prob_linear"].values
400
+
401
+ # uplift conversion
402
+ for treatment_key_i in treatment_name:
403
+ if (
404
+ treatment_key_i in delta_uplift_dict
405
+ and np.abs(delta_uplift_dict[treatment_key_i]) > 0.0
406
+ ):
407
+ treatment_index = df1.index[
408
+ df1["treatment_group_key"] == treatment_key_i
409
+ ].tolist()
410
+ # coefficient
411
+ coef_uplift = []
412
+ for ci in range(n_uplift_dict[treatment_key_i]):
413
+ coef_uplift.append(0.5)
414
+ x_uplift = df1.loc[
415
+ :, x_name_uplift_transformed_dict[treatment_key_i]
416
+ ].values
417
+ p2 = mean_dict[treatment_key_i]
418
+ a20 = np.log(p2 / (1.0 - p2)) - a1
419
+ xb_array = df1["conversion_prob_linear"].values + (
420
+ x_uplift * coef_uplift
421
+ ).sum(axis=1)
422
+ xb_array_treatment = xb_array[treatment_index]
423
+ a2 = fsolve(_softmax, a20, args=(p2, xb_array_treatment))[0]
424
+ df1["%s_conversion_prob_linear" % (treatment_key_i)] = a2 + xb_array
425
+ df1.loc[treatment_index, "conversion_prob_linear"] = df1.loc[
426
+ treatment_index, "%s_conversion_prob_linear" % (treatment_key_i)
427
+ ].values
428
+ else:
429
+ df1["%s_conversion_prob_linear" % (treatment_key_i)] = df1[
430
+ "conversion_prob_linear"
431
+ ].values
432
+
433
+ # generate conversion probability and true treatment effect ---------------------------------#
434
+ df1["conversion_prob"] = 1 / (1 + np.exp(-df1["conversion_prob_linear"].values))
435
+ df1["control_conversion_prob"] = 1 / (
436
+ 1 + np.exp(-df1["control_conversion_prob_linear"].values)
437
+ )
438
+ for treatment_key_i in treatment_name:
439
+ df1["%s_conversion_prob" % (treatment_key_i)] = 1 / (
440
+ 1 + np.exp(-df1["%s_conversion_prob_linear" % (treatment_key_i)].values)
441
+ )
442
+ df1["%s_true_effect" % (treatment_key_i)] = (
443
+ df1["%s_conversion_prob" % (treatment_key_i)].values
444
+ - df1["control_conversion_prob"].values
445
+ )
446
+
447
+ # generate Y ------------------------------------------------------------#
448
+ df1["conversion_prob"] = np.clip(df1["conversion_prob"].values, 0, 1)
449
+ df1[y_name] = np.random.binomial(1, df1["conversion_prob"].values)
450
+
451
+ return df1, x_name
452
+
453
+
454
+ def make_uplift_classification(
455
+ n_samples=1000,
456
+ treatment_name=["control", "treatment1", "treatment2", "treatment3"],
457
+ y_name="conversion",
458
+ n_classification_features=10,
459
+ n_classification_informative=5,
460
+ n_classification_redundant=0,
461
+ n_classification_repeated=0,
462
+ n_uplift_increase_dict={"treatment1": 2, "treatment2": 2, "treatment3": 2},
463
+ n_uplift_decrease_dict={"treatment1": 0, "treatment2": 0, "treatment3": 0},
464
+ delta_uplift_increase_dict={
465
+ "treatment1": 0.02,
466
+ "treatment2": 0.05,
467
+ "treatment3": 0.1,
468
+ },
469
+ delta_uplift_decrease_dict={
470
+ "treatment1": 0.0,
471
+ "treatment2": 0.0,
472
+ "treatment3": 0.0,
473
+ },
474
+ n_uplift_increase_mix_informative_dict={
475
+ "treatment1": 1,
476
+ "treatment2": 1,
477
+ "treatment3": 1,
478
+ },
479
+ n_uplift_decrease_mix_informative_dict={
480
+ "treatment1": 0,
481
+ "treatment2": 0,
482
+ "treatment3": 0,
483
+ },
484
+ positive_class_proportion=0.5,
485
+ random_seed=20190101,
486
+ ):
487
+ """Generate a synthetic dataset for classification uplift modeling problem.
488
+
489
+ Parameters
490
+ ----------
491
+ n_samples : int, optional (default=1000)
492
+ The number of samples to be generated for each treatment group.
493
+ treatment_name: list, optional (default = ['control','treatment1','treatment2','treatment3'])
494
+ The list of treatment names.
495
+ y_name: string, optional (default = 'conversion')
496
+ The name of the outcome variable to be used as a column in the output dataframe.
497
+ n_classification_features: int, optional (default = 10)
498
+ Total number of features for base classification
499
+ n_classification_informative: int, optional (default = 5)
500
+ Total number of informative features for base classification
501
+ n_classification_redundant: int, optional (default = 0)
502
+ Total number of redundant features for base classification
503
+ n_classification_repeated: int, optional (default = 0)
504
+ Total number of repeated features for base classification
505
+ n_uplift_increase_dict: dictionary, optional (default: {'treatment1': 2, 'treatment2': 2, 'treatment3': 2})
506
+ Number of features for generating positive treatment effects for corresponding treatment group.
507
+ Dictionary of {treatment_key: number_of_features_for_increase_uplift}.
508
+ n_uplift_decrease_dict: dictionary, optional (default: {'treatment1': 0, 'treatment2': 0, 'treatment3': 0})
509
+ Number of features for generating negative treatment effects for corresponding treatment group.
510
+ Dictionary of {treatment_key: number_of_features_for_increase_uplift}.
511
+ delta_uplift_increase_dict: dictionary, optional (default: {'treatment1': .02, 'treatment2': .05, 'treatment3': .1})
512
+ Positive treatment effect created by the positive uplift features on the base classification label.
513
+ Dictionary of {treatment_key: increase_delta}.
514
+ delta_uplift_decrease_dict: dictionary, optional (default: {'treatment1': 0., 'treatment2': 0., 'treatment3': 0.})
515
+ Negative treatment effect created by the negative uplift features on the base classification label.
516
+ Dictionary of {treatment_key: increase_delta}.
517
+ n_uplift_increase_mix_informative_dict: dictionary, optional
518
+ Number of positive mix features for each treatment. The positive mix feature is defined as a linear combination
519
+ of a randomly selected informative classification feature and a randomly selected positive uplift feature.
520
+ The linear combination is made by two coefficients sampled from a uniform distribution between -1 and 1.
521
+ default: {'treatment1': 1, 'treatment2': 1, 'treatment3': 1}
522
+ n_uplift_decrease_mix_informative_dict: dictionary, optional
523
+ Number of negative mix features for each treatment. The negative mix feature is defined as a linear combination
524
+ of a randomly selected informative classification feature and a randomly selected negative uplift feature. The
525
+ linear combination is made by two coefficients sampled from a uniform distribution between -1 and 1.
526
+ default: {'treatment1': 0, 'treatment2': 0, 'treatment3': 0}
527
+ positive_class_proportion: float, optional (default = 0.5)
528
+ The proportion of positive label (1) in the control group.
529
+ random_seed : int, optional (default = 20190101)
530
+ The random seed to be used in the data generation process.
531
+
532
+ Returns
533
+ -------
534
+ df_res : DataFrame
535
+ A data frame containing the treatment label, features, and outcome variable.
536
+ x_name : list
537
+ The list of feature names generated.
538
+
539
+ Notes
540
+ -----
541
+ The algorithm for generating the base classification dataset is adapted from the make_classification method in the
542
+ sklearn package, that uses the algorithm in Guyon [1] designed to generate the "Madelon" dataset.
543
+
544
+ References
545
+ ----------
546
+ .. [1] I. Guyon, "Design of experiments for the NIPS 2003 variable
547
+ selection benchmark", 2003.
548
+ """
549
+ # set seed
550
+ np.random.seed(seed=random_seed)
551
+
552
+ # create data frame
553
+ df_res = pd.DataFrame()
554
+
555
+ # generate treatment key
556
+ n_all = n_samples * len(treatment_name)
557
+ treatment_list = []
558
+ for ti in treatment_name:
559
+ treatment_list += [ti] * n_samples
560
+ treatment_list = np.random.permutation(treatment_list)
561
+ df_res["treatment_group_key"] = treatment_list
562
+
563
+ # generate features and labels
564
+ X1, Y1 = make_classification(
565
+ n_samples=n_all,
566
+ n_features=n_classification_features,
567
+ n_informative=n_classification_informative,
568
+ n_redundant=n_classification_redundant,
569
+ n_repeated=n_classification_repeated,
570
+ n_clusters_per_class=1,
571
+ weights=[1 - positive_class_proportion, positive_class_proportion],
572
+ )
573
+
574
+ x_name = []
575
+ x_informative_name = []
576
+ for xi in range(n_classification_informative):
577
+ x_name_i = "x" + str(len(x_name) + 1) + "_informative"
578
+ x_name.append(x_name_i)
579
+ x_informative_name.append(x_name_i)
580
+ df_res[x_name_i] = X1[:, xi]
581
+ for xi in range(n_classification_redundant):
582
+ x_name_i = "x" + str(len(x_name) + 1) + "_redundant"
583
+ x_name.append(x_name_i)
584
+ df_res[x_name_i] = X1[:, n_classification_informative + xi]
585
+ for xi in range(n_classification_repeated):
586
+ x_name_i = "x" + str(len(x_name) + 1) + "_repeated"
587
+ x_name.append(x_name_i)
588
+ df_res[x_name_i] = X1[
589
+ :, n_classification_informative + n_classification_redundant + xi
590
+ ]
591
+
592
+ for xi in range(
593
+ n_classification_features
594
+ - n_classification_informative
595
+ - n_classification_redundant
596
+ - n_classification_repeated
597
+ ):
598
+ x_name_i = "x" + str(len(x_name) + 1) + "_irrelevant"
599
+ x_name.append(x_name_i)
600
+ df_res[x_name_i] = np.random.normal(0, 1, n_all)
601
+
602
+ # default treatment effects
603
+ Y = Y1.copy()
604
+ Y_increase = np.zeros_like(Y1)
605
+ Y_decrease = np.zeros_like(Y1)
606
+
607
+ # generate uplift (positive)
608
+ for treatment_key_i in treatment_name:
609
+ treatment_index = df_res.index[
610
+ df_res["treatment_group_key"] == treatment_key_i
611
+ ].tolist()
612
+ if (
613
+ treatment_key_i in n_uplift_increase_dict
614
+ and n_uplift_increase_dict[treatment_key_i] > 0
615
+ ):
616
+ x_uplift_increase_name = []
617
+ adjust_class_proportion = (delta_uplift_increase_dict[treatment_key_i]) / (
618
+ 1 - positive_class_proportion
619
+ )
620
+ X_increase, Y_increase = make_classification(
621
+ n_samples=n_all,
622
+ n_features=n_uplift_increase_dict[treatment_key_i],
623
+ n_informative=n_uplift_increase_dict[treatment_key_i],
624
+ n_redundant=0,
625
+ n_clusters_per_class=1,
626
+ weights=[1 - adjust_class_proportion, adjust_class_proportion],
627
+ )
628
+ for xi in range(n_uplift_increase_dict[treatment_key_i]):
629
+ x_name_i = "x" + str(len(x_name) + 1) + "_uplift_increase"
630
+ x_name.append(x_name_i)
631
+ x_uplift_increase_name.append(x_name_i)
632
+ df_res[x_name_i] = X_increase[:, xi]
633
+ Y[treatment_index] = Y[treatment_index] + Y_increase[treatment_index]
634
+ if n_uplift_increase_mix_informative_dict[treatment_key_i] > 0:
635
+ for xi in range(
636
+ n_uplift_increase_mix_informative_dict[treatment_key_i]
637
+ ):
638
+ x_name_i = "x" + str(len(x_name) + 1) + "_increase_mix"
639
+ x_name.append(x_name_i)
640
+ df_res[x_name_i] = (
641
+ np.random.uniform(-1, 1)
642
+ * df_res[np.random.choice(x_informative_name)]
643
+ + np.random.uniform(-1, 1)
644
+ * df_res[np.random.choice(x_uplift_increase_name)]
645
+ )
646
+
647
+ # generate uplift (negative)
648
+ for treatment_key_i in treatment_name:
649
+ treatment_index = df_res.index[
650
+ df_res["treatment_group_key"] == treatment_key_i
651
+ ].tolist()
652
+ if (
653
+ treatment_key_i in n_uplift_decrease_dict
654
+ and n_uplift_decrease_dict[treatment_key_i] > 0
655
+ ):
656
+ x_uplift_decrease_name = []
657
+ adjust_class_proportion = (delta_uplift_decrease_dict[treatment_key_i]) / (
658
+ 1 - positive_class_proportion
659
+ )
660
+ X_decrease, Y_decrease = make_classification(
661
+ n_samples=n_all,
662
+ n_features=n_uplift_decrease_dict[treatment_key_i],
663
+ n_informative=n_uplift_decrease_dict[treatment_key_i],
664
+ n_redundant=0,
665
+ n_clusters_per_class=1,
666
+ weights=[1 - adjust_class_proportion, adjust_class_proportion],
667
+ )
668
+ for xi in range(n_uplift_decrease_dict[treatment_key_i]):
669
+ x_name_i = "x" + str(len(x_name) + 1) + "_uplift_decrease"
670
+ x_name.append(x_name_i)
671
+ x_uplift_decrease_name.append(x_name_i)
672
+ df_res[x_name_i] = X_decrease[:, xi]
673
+ Y[treatment_index] = Y[treatment_index] - Y_decrease[treatment_index]
674
+ if n_uplift_decrease_mix_informative_dict[treatment_key_i] > 0:
675
+ for xi in range(
676
+ n_uplift_decrease_mix_informative_dict[treatment_key_i]
677
+ ):
678
+ x_name_i = "x" + str(len(x_name) + 1) + "_decrease_mix"
679
+ x_name.append(x_name_i)
680
+ df_res[x_name_i] = (
681
+ np.random.uniform(-1, 1)
682
+ * df_res[np.random.choice(x_informative_name)]
683
+ + np.random.uniform(-1, 1)
684
+ * df_res[np.random.choice(x_uplift_decrease_name)]
685
+ )
686
+
687
+ # truncate Y
688
+ Y = np.clip(Y, 0, 1)
689
+
690
+ df_res[y_name] = Y
691
+ df_res["treatment_effect"] = Y - Y1
692
+ return df_res, x_name
causalml/source/causalml/dataset/regression.py ADDED
@@ -0,0 +1,209 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+
3
+ import numpy as np
4
+ from scipy.special import expit, logit
5
+
6
+ logger = logging.getLogger("causalml")
7
+
8
+
9
+ def synthetic_data(mode=1, n=1000, p=5, sigma=1.0, adj=0.0):
10
+ """ Synthetic data in Nie X. and Wager S. (2018) 'Quasi-Oracle Estimation of Heterogeneous Treatment Effects'
11
+ Args:
12
+ mode (int, optional): mode of the simulation: \
13
+ 1 for difficult nuisance components and an easy treatment effect. \
14
+ 2 for a randomized trial. \
15
+ 3 for an easy propensity and a difficult baseline. \
16
+ 4 for unrelated treatment and control groups. \
17
+ 5 for a hidden confounder biasing treatment.
18
+ n (int, optional): number of observations
19
+ p (int optional): number of covariates (>=5)
20
+ sigma (float): standard deviation of the error term
21
+ adj (float): adjustment term for the distribution of propensity, e. Higher values shift the distribution to 0.
22
+ It does not apply to mode == 2 or 3.
23
+ Returns:
24
+ (tuple): Synthetically generated samples with the following outputs:
25
+ - y ((n,)-array): outcome variable.
26
+ - X ((n,p)-ndarray): independent variables.
27
+ - w ((n,)-array): treatment flag with value 0 or 1.
28
+ - tau ((n,)-array): individual treatment effect.
29
+ - b ((n,)-array): expected outcome.
30
+ - e ((n,)-array): propensity of receiving treatment.
31
+ """
32
+
33
+ catalog = {
34
+ 1: simulate_nuisance_and_easy_treatment,
35
+ 2: simulate_randomized_trial,
36
+ 3: simulate_easy_propensity_difficult_baseline,
37
+ 4: simulate_unrelated_treatment_control,
38
+ 5: simulate_hidden_confounder,
39
+ }
40
+
41
+ assert mode in catalog, "Invalid mode {}. Should be one of {}".format(
42
+ mode, set(catalog)
43
+ )
44
+ return catalog[mode](n, p, sigma, adj)
45
+
46
+
47
+ def simulate_nuisance_and_easy_treatment(n=1000, p=5, sigma=1.0, adj=0.0):
48
+ """Synthetic data with a difficult nuisance components and an easy treatment effect
49
+ From Setup A in Nie X. and Wager S. (2018) 'Quasi-Oracle Estimation of Heterogeneous Treatment Effects'
50
+ Args:
51
+ n (int, optional): number of observations
52
+ p (int optional): number of covariates (>=5)
53
+ sigma (float): standard deviation of the error term
54
+ adj (float): adjustment term for the distribution of propensity, e. Higher values shift the distribution to 0.
55
+ Returns:
56
+ (tuple): Synthetically generated samples with the following outputs:
57
+ - y ((n,)-array): outcome variable.
58
+ - X ((n,p)-ndarray): independent variables.
59
+ - w ((n,)-array): treatment flag with value 0 or 1.
60
+ - tau ((n,)-array): individual treatment effect.
61
+ - b ((n,)-array): expected outcome.
62
+ - e ((n,)-array): propensity of receiving treatment.
63
+ """
64
+
65
+ X = np.random.uniform(size=n * p).reshape((n, -1))
66
+ b = (
67
+ np.sin(np.pi * X[:, 0] * X[:, 1])
68
+ + 2 * (X[:, 2] - 0.5) ** 2
69
+ + X[:, 3]
70
+ + 0.5 * X[:, 4]
71
+ )
72
+ eta = 0.1
73
+ e = np.maximum(
74
+ np.repeat(eta, n),
75
+ np.minimum(np.sin(np.pi * X[:, 0] * X[:, 1]), np.repeat(1 - eta, n)),
76
+ )
77
+ e = expit(logit(e) - adj)
78
+ tau = (X[:, 0] + X[:, 1]) / 2
79
+
80
+ w = np.random.binomial(1, e, size=n)
81
+ y = b + (w - 0.5) * tau + sigma * np.random.normal(size=n)
82
+
83
+ return y, X, w, tau, b, e
84
+
85
+
86
+ def simulate_randomized_trial(n=1000, p=5, sigma=1.0, adj=0.0):
87
+ """Synthetic data of a randomized trial
88
+ From Setup B in Nie X. and Wager S. (2018) 'Quasi-Oracle Estimation of Heterogeneous Treatment Effects'
89
+ Args:
90
+ n (int, optional): number of observations
91
+ p (int optional): number of covariates (>=5)
92
+ sigma (float): standard deviation of the error term
93
+ adj (float): no effect. added for consistency
94
+ Returns:
95
+ (tuple): Synthetically generated samples with the following outputs:
96
+ - y ((n,)-array): outcome variable.
97
+ - X ((n,p)-ndarray): independent variables.
98
+ - w ((n,)-array): treatment flag with value 0 or 1.
99
+ - tau ((n,)-array): individual treatment effect.
100
+ - b ((n,)-array): expected outcome.
101
+ - e ((n,)-array): propensity of receiving treatment.
102
+ """
103
+
104
+ X = np.random.normal(size=n * p).reshape((n, -1))
105
+ b = np.maximum.reduce([np.repeat(0.0, n), X[:, 0] + X[:, 1], X[:, 2]]) + np.maximum(
106
+ np.repeat(0.0, n), X[:, 3] + X[:, 4]
107
+ )
108
+ e = np.repeat(0.5, n)
109
+ tau = X[:, 0] + np.log1p(np.exp(X[:, 1]))
110
+
111
+ w = np.random.binomial(1, e, size=n)
112
+ y = b + (w - 0.5) * tau + sigma * np.random.normal(size=n)
113
+
114
+ return y, X, w, tau, b, e
115
+
116
+
117
+ def simulate_easy_propensity_difficult_baseline(n=1000, p=5, sigma=1.0, adj=0.0):
118
+ """Synthetic data with easy propensity and a difficult baseline
119
+ From Setup C in Nie X. and Wager S. (2018) 'Quasi-Oracle Estimation of Heterogeneous Treatment Effects'
120
+ Args:
121
+ n (int, optional): number of observations
122
+ p (int optional): number of covariates (>=3)
123
+ sigma (float): standard deviation of the error term
124
+ adj (float): no effect. added for consistency
125
+ Returns:
126
+ (tuple): Synthetically generated samples with the following outputs:
127
+ - y ((n,)-array): outcome variable.
128
+ - X ((n,p)-ndarray): independent variables.
129
+ - w ((n,)-array): treatment flag with value 0 or 1.
130
+ - tau ((n,)-array): individual treatment effect.
131
+ - b ((n,)-array): expected outcome.
132
+ - e ((n,)-array): propensity of receiving treatment.
133
+ """
134
+
135
+ X = np.random.normal(size=n * p).reshape((n, -1))
136
+ b = 2 * np.log1p(np.exp(X[:, 0] + X[:, 1] + X[:, 2]))
137
+ e = 1 / (1 + np.exp(X[:, 1] + X[:, 2]))
138
+ tau = np.repeat(1.0, n)
139
+
140
+ w = np.random.binomial(1, e, size=n)
141
+ y = b + (w - 0.5) * tau + sigma * np.random.normal(size=n)
142
+
143
+ return y, X, w, tau, b, e
144
+
145
+
146
+ def simulate_unrelated_treatment_control(n=1000, p=5, sigma=1.0, adj=0.0):
147
+ """Synthetic data with unrelated treatment and control groups.
148
+ From Setup D in Nie X. and Wager S. (2018) 'Quasi-Oracle Estimation of Heterogeneous Treatment Effects'
149
+ Args:
150
+ n (int, optional): number of observations
151
+ p (int optional): number of covariates (>=3)
152
+ sigma (float): standard deviation of the error term
153
+ adj (float): adjustment term for the distribution of propensity, e. Higher values shift the distribution to 0.
154
+ Returns:
155
+ (tuple): Synthetically generated samples with the following outputs:
156
+ - y ((n,)-array): outcome variable.
157
+ - X ((n,p)-ndarray): independent variables.
158
+ - w ((n,)-array): treatment flag with value 0 or 1.
159
+ - tau ((n,)-array): individual treatment effect.
160
+ - b ((n,)-array): expected outcome.
161
+ - e ((n,)-array): propensity of receiving treatment.
162
+ """
163
+
164
+ X = np.random.normal(size=n * p).reshape((n, -1))
165
+ b = (
166
+ np.maximum(np.repeat(0.0, n), X[:, 0] + X[:, 1] + X[:, 2])
167
+ + np.maximum(np.repeat(0.0, n), X[:, 3] + X[:, 4])
168
+ ) / 2
169
+ e = 1 / (1 + np.exp(-X[:, 0]) + np.exp(-X[:, 1]))
170
+ e = expit(logit(e) - adj)
171
+ tau = np.maximum(np.repeat(0.0, n), X[:, 0] + X[:, 1] + X[:, 2]) - np.maximum(
172
+ np.repeat(0.0, n), X[:, 3] + X[:, 4]
173
+ )
174
+
175
+ w = np.random.binomial(1, e, size=n)
176
+ y = b + (w - 0.5) * tau + sigma * np.random.normal(size=n)
177
+
178
+ return y, X, w, tau, b, e
179
+
180
+
181
+ def simulate_hidden_confounder(n=10000, p=5, sigma=1.0, adj=0.0):
182
+ """Synthetic dataset with a hidden confounder biasing treatment.
183
+ From Louizos et al. (2018) "Causal Effect Inference with Deep Latent-Variable Models"
184
+ Args:
185
+ n (int, optional): number of observations
186
+ p (int optional): number of covariates (>=3)
187
+ sigma (float): standard deviation of the error term
188
+ adj (float): no effect. added for consistency
189
+ Returns:
190
+ (tuple): Synthetically generated samples with the following outputs:
191
+ - y ((n,)-array): outcome variable.
192
+ - X ((n,p)-ndarray): independent variables.
193
+ - w ((n,)-array): treatment flag with value 0 or 1.
194
+ - tau ((n,)-array): individual treatment effect.
195
+ - b ((n,)-array): expected outcome.
196
+ - e ((n,)-array): propensity of receiving treatment.
197
+ """
198
+ z = np.random.binomial(1, 0.5, size=n).astype(np.double)
199
+ X = np.random.normal(z, 5 * z + 3 * (1 - z), size=(p, n)).T
200
+ e = 0.75 * z + 0.25 * (1 - z)
201
+ w = np.random.binomial(1, e)
202
+ b = expit(3 * (z + 2 * (2 * w - 2)))
203
+ y = np.random.binomial(1, b)
204
+
205
+ # Compute true ite tau for evaluation (via Monte Carlo approximation).
206
+ t0_t1 = np.array([[0.0], [1.0]])
207
+ y_t0, y_t1 = expit(3 * (z + 2 * (2 * t0_t1 - 2)))
208
+ tau = y_t1 - y_t0
209
+ return y, X, w, tau, b, e
causalml/source/causalml/dataset/semiSynthetic.py ADDED
@@ -0,0 +1,1056 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Synthetic Validation Dataset Generator according to the paper: "Synth-Validation: Selecting the Best Causal Inference Method for a Given Dataset"
2
+ # https://arxiv.org/pdf/1711.00083
3
+
4
+ import numpy as np
5
+ import pandas as pd
6
+ from scipy.optimize import minimize
7
+ from sklearn.tree import DecisionTreeRegressor
8
+ from sklearn.ensemble import RandomForestRegressor
9
+ from typing import Callable, List, Optional, Union
10
+ from numpy.typing import ArrayLike
11
+ import multiprocessing as mp
12
+ from functools import partial
13
+ from sklearn.linear_model import LinearRegression
14
+ from causalml.inference.meta import BaseXRegressor, BaseTRegressor
15
+ from scipy.special import expit
16
+
17
+
18
+ class SemiSynthDataGenerator:
19
+ def __init__(
20
+ self,
21
+ Q: int = 5,
22
+ gamma: float = 2.0,
23
+ train_frac: float = 0.8,
24
+ val_frac: float = 0.1,
25
+ B: int = 5,
26
+ maxdepths: List[int] = [1, 2, 3],
27
+ lambdas: List[float] = np.logspace(-5, 1, num=5).tolist(),
28
+ M: int = 30,
29
+ early_stopping_rounds: int = 3,
30
+ verbose: bool = False,
31
+ **kwargs,
32
+ ):
33
+ self.Q = Q
34
+ self.gamma = gamma
35
+ self.train_frac = train_frac
36
+ self.val_frac = val_frac
37
+ self.B = B
38
+ self.maxdepths = maxdepths
39
+ self.lambdas = lambdas
40
+ self.M = M
41
+ self.early_stopping_rounds = early_stopping_rounds
42
+ self.verbose = verbose
43
+ self.kwargs = kwargs
44
+
45
+ def fit(
46
+ self,
47
+ X: pd.DataFrame,
48
+ w: pd.Series,
49
+ y: pd.Series,
50
+ initial_taus: Optional[List[float]] = None,
51
+ ):
52
+ self.X = X
53
+ self.y = y
54
+ self.w = w
55
+ np.random.seed(42)
56
+ if initial_taus is None:
57
+ # raw_tau
58
+ raw_tau = y[w == 1].mean() - y[w == 0].mean()
59
+ # lm_tau
60
+ X_lm = pd.concat([w, X], axis=1)
61
+ lm = LinearRegression().fit(X_lm, y)
62
+ lm_tau = lm.coef_[0]
63
+ # x_learner_tau
64
+ x_learner = BaseXRegressor(DecisionTreeRegressor())
65
+ x_learner_tau = x_learner.estimate_ate(X=X, treatment=w, y=y)[0]
66
+ # t_learner_tau
67
+ t_learner = BaseTRegressor(RandomForestRegressor())
68
+ t_learner_tau = t_learner.estimate_ate(X=X, treatment=w, y=y)[0]
69
+ initial_taus = [
70
+ float(raw_tau),
71
+ float(lm_tau),
72
+ float(x_learner_tau),
73
+ float(t_learner_tau),
74
+ ]
75
+ else:
76
+ initial_taus = [float(t) for t in initial_taus]
77
+ initial_taus_arr = np.array(initial_taus, dtype=float)
78
+ initial_taus_range = initial_taus_arr.max() - initial_taus_arr.min()
79
+ initial_taus_median = np.median(initial_taus_arr)
80
+ taus = np.linspace(
81
+ initial_taus_median - self.gamma * initial_taus_range,
82
+ initial_taus_median + self.gamma * initial_taus_range,
83
+ self.Q,
84
+ )
85
+ self.taus = taus
86
+ self.dgps = []
87
+ for real_tau in taus:
88
+ self.dgps.append(
89
+ miu_cv(
90
+ y=np.asarray(self.y),
91
+ w=np.asarray(self.w),
92
+ X=self.X,
93
+ real_tau=real_tau,
94
+ train_frac=self.train_frac,
95
+ val_frac=self.val_frac,
96
+ B=self.B,
97
+ max_depths=self.maxdepths,
98
+ lambdas=self.lambdas,
99
+ M=self.M,
100
+ early_stopping_rounds=self.early_stopping_rounds,
101
+ verbose=self.verbose,
102
+ **self.kwargs,
103
+ )
104
+ )
105
+
106
+ def generate(self, K: int = 10, n=None) -> List[List[pd.DataFrame]]:
107
+ if n is None:
108
+ n = len(self.X)
109
+ if all((self.y == 0) | (self.y == 1)):
110
+ binary_y = True
111
+ else:
112
+ binary_y = False
113
+ ctrl_idx = np.where(self.w == 0)[0]
114
+ trt_idx = np.where(self.w == 1)[0]
115
+ ctrl_n = int(n * (len(ctrl_idx) / len(self.X)))
116
+ trt_n = int(n * (len(trt_idx) / len(self.X)))
117
+ ans = []
118
+ for q in range(len(self.dgps)):
119
+ datasets = []
120
+ dgp_q = self.dgps[q]["final_model"]
121
+ data_tau = self.X.copy()
122
+ y0 = dgp_q[0](data_tau)
123
+ y1 = dgp_q[1](data_tau)
124
+ if binary_y:
125
+ y0 = logistic(y0)
126
+ y1 = logistic(y1)
127
+ data_tau["w"] = self.w
128
+ data_tau["tau_i"] = y1 - y0
129
+ data_tau["y_w"] = np.where(self.w == 1, y1, y0)
130
+ resid = self.y - data_tau["y_w"]
131
+ for k in range(K):
132
+ rng = np.random.default_rng(seed=k)
133
+ ctrl_idx_qk = rng.choice(ctrl_idx, size=ctrl_n, replace=True)
134
+ trt_idx_qk = rng.choice(trt_idx, size=trt_n, replace=True)
135
+ idx = np.concatenate([ctrl_idx_qk, trt_idx_qk])
136
+ data_qk = data_tau.iloc[idx].copy()
137
+ if not binary_y:
138
+ data_qk["y"] = data_qk["y_w"] + rng.choice(
139
+ resid, size=len(data_qk), replace=True
140
+ ) # aka observed y
141
+ else:
142
+ data_qk["y"] = data_qk["y_w"].apply(lambda x: rng.binomial(1, x))
143
+ data_qk = data_qk[["y", "w", "tau_i"] + list(self.X)]
144
+ datasets.append(data_qk)
145
+ ans.append(datasets)
146
+ return ans
147
+
148
+
149
+ def continuous_objective(x, Q, a, d):
150
+ """
151
+ Compute the continuous objective function for quadratic optimization.
152
+
153
+ Parameters:
154
+ -----------
155
+ x : np.ndarray
156
+ The variable vector to optimize over.
157
+ Q : np.ndarray
158
+ The quadratic coefficient matrix.
159
+ a : np.ndarray
160
+ The linear coefficient vector.
161
+ d : float
162
+ The constant term.
163
+
164
+ Returns:
165
+ --------
166
+ float
167
+ The value of the objective function: x^T Q x + a^T x + d
168
+ """
169
+ return np.dot(x, Q @ x) + np.dot(a, x) + d
170
+
171
+
172
+ def deviance(y, pred):
173
+ """
174
+ Compute the binomial deviance loss function.
175
+
176
+ Parameters:
177
+ -----------
178
+ y : np.ndarray
179
+ True binary outcomes (0 or 1).
180
+ pred : np.ndarray
181
+ Predicted logits.
182
+
183
+ Returns:
184
+ --------
185
+ float
186
+ The binomial deviance loss: -2 * mean(y * pred - log(1 + exp(pred)))
187
+ """
188
+ return -2.0 * np.mean((y * pred) - np.logaddexp(0.0, pred))
189
+
190
+
191
+ def logit(x):
192
+ """
193
+ Compute the logit (log-odds) transformation.
194
+
195
+ Parameters:
196
+ -----------
197
+ x : np.ndarray
198
+ Input values between 0 and 1.
199
+
200
+ Returns:
201
+ --------
202
+ np.ndarray
203
+ Logit-transformed values: log(x / (1 - x))
204
+ """
205
+ return np.log(x / (1 - x))
206
+
207
+
208
+ def logistic(x):
209
+ """
210
+ Compute the logistic (sigmoid) transformation.
211
+
212
+ Parameters:
213
+ -----------
214
+ x : np.ndarray
215
+ Input values (can be any real number).
216
+
217
+ Returns:
218
+ --------
219
+ np.ndarray
220
+ Logistic-transformed values: 1 / (1 + exp(-x))
221
+ """
222
+ return 1 / (1 + np.exp(-x))
223
+
224
+
225
+ def binary_objective(x, w, y):
226
+ """
227
+ Compute the binary objective function for treatment effect estimation.
228
+
229
+ Parameters:
230
+ -----------
231
+ x : np.ndarray
232
+ Parameter vector [x0, x1] where x0 is for control group, x1 for treatment group.
233
+ w : np.ndarray
234
+ Treatment assignment vector (0 for control, 1 for treatment).
235
+ y : np.ndarray
236
+ Binary outcome vector.
237
+
238
+ Returns:
239
+ --------
240
+ float
241
+ The binary deviance loss for the given parameters.
242
+ """
243
+ pred = np.where(w == 0, x[0], x[1])
244
+ return deviance(y, pred)
245
+
246
+
247
+ def negative_gradient(y, pred):
248
+ """
249
+ Compute the negative gradient for binary outcomes.
250
+
251
+ Parameters:
252
+ -----------
253
+ y : np.ndarray
254
+ True binary outcomes (0 or 1).
255
+ pred : np.ndarray
256
+ Predicted logits.
257
+
258
+ Returns:
259
+ --------
260
+ np.ndarray
261
+ The negative gradient: y - losgistic_sigmoid(pred)
262
+ """
263
+ return y - expit(pred.ravel())
264
+
265
+
266
+ def miu_m(
267
+ y: ArrayLike,
268
+ w: ArrayLike,
269
+ X: Union[pd.DataFrame, ArrayLike],
270
+ real_tau: Optional[float] = None,
271
+ miu_m_minus_1: Optional[List[Callable]] = None,
272
+ val_y: Optional[ArrayLike] = None,
273
+ val_w: Optional[ArrayLike] = None,
274
+ val_X: Optional[Union[pd.DataFrame, ArrayLike]] = None,
275
+ max_depth: Union[int, float] = 3,
276
+ lambda_: float = 0.0,
277
+ **tree_args,
278
+ ) -> List[Callable]:
279
+ """
280
+ Build the m-th iteration of the MIU (Model-based Imputation with Uncertainty) ensemble.
281
+
282
+ This function implements a single iteration of the MIU algorithm, which builds
283
+ treatment-specific models while maintaining a constraint on the treatment effect.
284
+
285
+ Parameters:
286
+ -----------
287
+ y : ArrayLike
288
+ Outcome array. Can be continuous or binary (0/1). Will be converted to np.ndarray.
289
+ w : ArrayLike
290
+ Treatment assignment array (0 for control, 1 for treatment). Will be converted to np.ndarray.
291
+ X : Union[pd.DataFrame, ArrayLike]
292
+ Covariate matrix for training the models. Will be converted to pd.DataFrame.
293
+ real_tau : Optional[float], default=None
294
+ The true treatment effect to constrain the model. Required for m=1.
295
+ miu_m_minus_1 : Optional[List[Callable]], default=None
296
+ List of two functions [miu_0, miu_1] from the previous iteration.
297
+ If None, this is the first iteration (m=1).
298
+ val_y : Optional[ArrayLike], default=None
299
+ Validation outcome array. Used for constraint calculation if provided.
300
+ val_w : Optional[ArrayLike], default=None
301
+ Validation treatment assignment array.
302
+ val_X : Optional[Union[pd.DataFrame, ArrayLike]], default=None
303
+ Validation covariate matrix. Used for constraint calculation if provided.
304
+ max_depth : Union[int, float], default=3
305
+ Maximum depth of the decision trees used in this iteration.
306
+ lambda_ : float, default=0.0
307
+ L2 regularization parameter for the leaf values.
308
+ **tree_args
309
+ Additional arguments passed to DecisionTreeRegressor.
310
+
311
+ Returns:
312
+ --------
313
+ List[Callable]
314
+ List containing two functions [miu_0_m, miu_1_m]:
315
+ - miu_0_m: Function that predicts outcomes for control group (w=0)
316
+ - miu_1_m: Function that predicts outcomes for treatment group (w=1)
317
+
318
+ Notes:
319
+ ------
320
+ - For m=1, the function fits simple constant models with treatment effect constraint
321
+ - For m>1, the function fits regression trees to residuals from previous iteration
322
+ - The treatment effect constraint ensures honest estimation of treatment effects
323
+ - Binary outcomes use logistic regression, continuous outcomes use linear regression
324
+ """
325
+ # Convert inputs to appropriate types
326
+ y = np.asarray(y)
327
+ w = np.asarray(w)
328
+
329
+ if not isinstance(X, pd.DataFrame):
330
+ X = pd.DataFrame(X)
331
+
332
+ if val_y is not None:
333
+ val_y = np.asarray(val_y)
334
+ if val_w is not None:
335
+ val_w = np.asarray(val_w)
336
+ if val_X is not None and not isinstance(val_X, pd.DataFrame):
337
+ val_X = pd.DataFrame(val_X)
338
+
339
+ if all((y == 0) | (y == 1)):
340
+ binary_y = True
341
+ else:
342
+ binary_y = False
343
+ if miu_m_minus_1 is None:
344
+ # m == 1
345
+ if real_tau is None:
346
+ raise ValueError("For m=1 (first call to miu_m) real_tau must be supplied")
347
+ x0 = np.zeros(2)
348
+ if not binary_y:
349
+ n0 = (w == 0).sum()
350
+ n1 = (w == 1).sum()
351
+ Q = np.array([[n0, 0], [0, n1]])
352
+ a = np.array(
353
+ [
354
+ -2 * y[w == 0].sum(),
355
+ -2 * y[w == 1].sum(),
356
+ ]
357
+ )
358
+ d = (y**2).sum()
359
+
360
+ constraints = {"type": "eq", "fun": lambda x: x[1] - x[0] - real_tau}
361
+ res = minimize(
362
+ fun=continuous_objective,
363
+ x0=x0,
364
+ args=(Q, a, d),
365
+ constraints=constraints,
366
+ method="SLSQP",
367
+ )
368
+ else:
369
+ constraints = {
370
+ "type": "eq",
371
+ "fun": lambda x: logistic(x[1]) - logistic(x[0]) - real_tau,
372
+ }
373
+ res = minimize(
374
+ fun=binary_objective,
375
+ x0=x0,
376
+ args=(w, y),
377
+ constraints=constraints,
378
+ method="SLSQP",
379
+ )
380
+
381
+ res01, res11 = res.x[0], res.x[1]
382
+
383
+ def miu_01(x):
384
+ return np.repeat(res01, len(x))
385
+
386
+ def miu_11(x):
387
+ return np.repeat(res11, len(x))
388
+
389
+ return [miu_01, miu_11]
390
+ else:
391
+ # m > 1
392
+ miu_0_m_minus_1, miu_1_m_minus_1 = miu_m_minus_1
393
+ # Predict y_hat using previous miu functions
394
+ y_1 = miu_1_m_minus_1(X)
395
+ y_0 = miu_0_m_minus_1(X)
396
+ y_hat = np.where(w == 1, y_1, y_0)
397
+ if not binary_y:
398
+ resid = y - y_hat
399
+ else:
400
+ resid = negative_gradient(y, y_hat)
401
+ treat = w == 1
402
+ # Fit regression trees to residuals
403
+ b_0m = DecisionTreeRegressor(max_depth=max_depth, **tree_args, random_state=42)
404
+ b_0m.fit(X.loc[~treat], resid[~treat])
405
+ b_1m = DecisionTreeRegressor(max_depth=max_depth, **tree_args, random_state=42)
406
+ b_1m.fit(X.loc[treat], resid[treat])
407
+ # Predict leaf node for each sample
408
+ R0 = b_0m.apply(X)
409
+ R1 = b_1m.apply(X)
410
+ if not binary_y:
411
+ # Group sizes and residuals
412
+ resid0 = (
413
+ pd.Series(resid)
414
+ .groupby(R0)
415
+ .agg(["count", "sum"])
416
+ .reset_index()
417
+ .rename(columns={"index": "leaf_node"})
418
+ )
419
+ resid1 = (
420
+ pd.Series(resid)
421
+ .groupby(R1)
422
+ .agg(["count", "sum"])
423
+ .reset_index()
424
+ .rename(columns={"index": "leaf_node"})
425
+ )
426
+ num_params = len(resid0) + len(resid1)
427
+ Q = np.diag(
428
+ np.concatenate([resid0["count"].to_numpy(), resid1["count"].to_numpy()])
429
+ * lambda_
430
+ )
431
+ a = -2 * np.concatenate(
432
+ [resid0["sum"].to_numpy(), resid1["sum"].to_numpy()]
433
+ )
434
+ d = (resid**2).sum()
435
+ if val_X is not None and val_y is not None and val_w is not None:
436
+ # Optionally add validation data
437
+ X_full = pd.concat([X, val_X], ignore_index=True, axis=0)
438
+ R0 = b_0m.apply(X_full)
439
+ R1 = b_1m.apply(X_full)
440
+ # Making the constraint apply over the entire dataset - this is still honest
441
+ resid0 = (
442
+ pd.Series(R0)
443
+ .groupby(R0)
444
+ .agg(["count"])
445
+ .reset_index()
446
+ .rename(columns={"index": "leaf_node"})
447
+ )
448
+ resid1 = (
449
+ pd.Series(R1)
450
+ .groupby(R1)
451
+ .agg(["count"])
452
+ .reset_index()
453
+ .rename(columns={"index": "leaf_node"})
454
+ )
455
+
456
+ constraints = {
457
+ "type": "eq",
458
+ "fun": lambda x: np.dot(resid1["count"].to_numpy(), x[len(resid0) :])
459
+ - np.dot(resid0["count"].to_numpy(), x[: len(resid0)]),
460
+ }
461
+ x0 = np.zeros(num_params)
462
+ res = minimize(
463
+ continuous_objective,
464
+ x0,
465
+ args=(Q, a, d),
466
+ constraints=constraints,
467
+ method="SLSQP",
468
+ )
469
+ else:
470
+ resid0 = pd.DataFrame({"leaf_node": np.unique(R0)})
471
+ resid1 = pd.DataFrame({"leaf_node": np.unique(R1)})
472
+ x0 = np.zeros(len(resid0) + len(resid1))
473
+
474
+ def binary_objective_m(x, y, w, R0, R1, lambda_):
475
+ loss = []
476
+ r0 = np.unique(R0)
477
+ r1 = np.unique(R1)
478
+ ctrl_nodes = len(r0)
479
+ for i in range(len(x)):
480
+ if i < ctrl_nodes - 1:
481
+ idx = (R0 == r0[i]) & (w == 0)
482
+ else:
483
+ idx = (R1 == r1[i - ctrl_nodes]) & (w == 1)
484
+ pred = np.full(sum(idx), x[i])
485
+ loss.append(deviance(y[idx], pred) + sum(idx) * lambda_ * x[i] ** 2)
486
+ return np.array(loss).sum()
487
+
488
+ if val_X is not None and val_y is not None and val_w is not None:
489
+ # Optionally add validation data
490
+ X_full = pd.concat([X, val_X], ignore_index=True, axis=0)
491
+ R0_constraint = b_0m.apply(X_full)
492
+ R1_constraint = b_1m.apply(X_full)
493
+ prev0 = miu_0_m_minus_1(X_full)
494
+ prev1 = miu_1_m_minus_1(X_full)
495
+ # Making the constraint apply over the entire dataset - this is still honest
496
+ else:
497
+ R0_constraint = R0
498
+ R1_constraint = R1
499
+ prev0 = y_0
500
+ prev1 = y_1
501
+
502
+ real_tau = (logistic(prev1) - logistic(prev0)).mean()
503
+
504
+ def con_m(x, R0_constraint, R1_constraint, prev0, prev1):
505
+ group_sum = []
506
+ r0 = np.unique(R0_constraint)
507
+ r1 = np.unique(R1_constraint)
508
+ ctrl_nodes = len(r0)
509
+ for i in range(len(x)):
510
+ if i < ctrl_nodes:
511
+ idx = R0_constraint == r0[i]
512
+ group_sum.append(
513
+ (logistic(prev0[idx] + x[i])).sum()
514
+ / len(R0_constraint)
515
+ * -1
516
+ )
517
+ else:
518
+ idx = R1_constraint == r1[i - ctrl_nodes]
519
+ group_sum.append(
520
+ (logistic(prev1[idx] + x[i])).sum() / len(R0_constraint)
521
+ )
522
+ return np.array(group_sum).sum() - real_tau
523
+
524
+ constraints = {
525
+ "type": "eq",
526
+ "fun": lambda x: con_m(x, R0_constraint, R1_constraint, prev0, prev1),
527
+ }
528
+
529
+ res = minimize(
530
+ fun=binary_objective_m,
531
+ x0=x0,
532
+ args=(y, w, R0, R1, lambda_),
533
+ constraints=constraints,
534
+ method="SLSQP",
535
+ )
536
+
537
+ # Assign fitted values to leaves
538
+ resid0["leaf_value"] = res.x[: len(resid0)]
539
+ resid1["leaf_value"] = res.x[len(resid0) :]
540
+
541
+ def miu_0m(x):
542
+ prev = miu_0_m_minus_1(x)
543
+ leaves = pd.DataFrame({"leaf_node": b_0m.apply(x)})
544
+ return (
545
+ prev
546
+ + leaves.merge(resid0, on="leaf_node", how="left")[
547
+ "leaf_value"
548
+ ].to_numpy()
549
+ )
550
+
551
+ def miu_1m(x):
552
+ prev = miu_1_m_minus_1(x)
553
+ leaves = pd.DataFrame({"leaf_node": b_1m.apply(x)})
554
+ return (
555
+ prev
556
+ + leaves.merge(resid1, on="leaf_node", how="left")[
557
+ "leaf_value"
558
+ ].to_numpy()
559
+ )
560
+
561
+ return [miu_0m, miu_1m]
562
+
563
+
564
+ def miu(
565
+ y: ArrayLike,
566
+ w: ArrayLike,
567
+ X: Union[pd.DataFrame, ArrayLike],
568
+ real_tau: float,
569
+ val_y: Optional[ArrayLike] = None,
570
+ val_w: Optional[ArrayLike] = None,
571
+ val_X: Optional[Union[pd.DataFrame, ArrayLike]] = None,
572
+ max_depth: Union[int, float] = 3,
573
+ lambda_: float = 0.0,
574
+ M: int = 10,
575
+ early_stopping_rounds: Union[int, float] = float("inf"),
576
+ verbose: bool = False,
577
+ **tree_args,
578
+ ) -> dict:
579
+ """
580
+ Train an ensemble of M MIU models and return the best one.
581
+
582
+ This function implements the complete MIU (Model-based Imputation with Uncertainty)
583
+ algorithm, which builds an ensemble of treatment-specific models while maintaining
584
+ constraints on the treatment effect for honest estimation.
585
+
586
+ Parameters:
587
+ -----------
588
+ y : ArrayLike
589
+ Outcome array. Can be continuous or binary (0/1). Will be converted to np.ndarray.
590
+ w : ArrayLike
591
+ Treatment assignment array (0 for control, 1 for treatment). Will be converted to np.ndarray.
592
+ X : Union[pd.DataFrame, ArrayLike]
593
+ Covariate matrix for training the models. Will be converted to pd.DataFrame.
594
+ real_tau : float
595
+ The true treatment effect to constrain the model. This is used to ensure
596
+ honest estimation of treatment effects.
597
+ val_y : Optional[ArrayLike], default=None
598
+ Validation outcome array. Used for early stopping and model selection.
599
+ val_w : Optional[ArrayLike], default=None
600
+ Validation treatment assignment array.
601
+ val_X : Optional[Union[pd.DataFrame, ArrayLike]], default=None
602
+ Validation covariate matrix. Used for early stopping and model selection.
603
+ max_depth : Union[int, float], default=3
604
+ Maximum depth of the decision trees used in each iteration.
605
+ lambda_ : float, default=0.0
606
+ L2 regularization parameter for the leaf values in each iteration.
607
+ M : int, default=10
608
+ Maximum number of ensemble iterations to perform.
609
+ early_stopping_rounds : Union[int, float], default=float('inf')
610
+ Number of rounds without improvement before stopping early.
611
+ If val_X is None, this must be float('inf').
612
+ verbose : bool, default=False
613
+ Whether to print progress information during training.
614
+ **tree_args
615
+ Additional arguments passed to DecisionTreeRegressor in each iteration.
616
+
617
+ Returns:
618
+ --------
619
+ dict
620
+ Dictionary containing:
621
+ - 'best_model': List[Callable] - The best ensemble model [miu_0, miu_1]
622
+ - 'loss': np.ndarray - Array of validation losses for each iteration
623
+ - 'best_model_m': int - The iteration number of the best model
624
+
625
+ Notes:
626
+ ------
627
+ - The algorithm builds an ensemble by iteratively fitting models to residuals
628
+ - Each iteration maintains the treatment effect constraint using real_tau
629
+ - Early stopping is based on validation loss if validation data is provided
630
+ - The best model is selected based on validation loss or training loss
631
+ - Binary outcomes use logistic regression, continuous outcomes use linear regression
632
+ """
633
+ if val_X is None and not np.isinf(early_stopping_rounds):
634
+ raise ValueError("If val_X is None then early_stopping_rounds must be Inf")
635
+
636
+ # Convert inputs to appropriate types
637
+ y = np.asarray(y)
638
+ w = np.asarray(w)
639
+
640
+ if not isinstance(X, pd.DataFrame):
641
+ X = pd.DataFrame(X)
642
+
643
+ if val_y is not None:
644
+ val_y = np.asarray(val_y)
645
+ if val_w is not None:
646
+ val_w = np.asarray(val_w)
647
+ if val_X is not None and not isinstance(val_X, pd.DataFrame):
648
+ val_X = pd.DataFrame(val_X)
649
+
650
+ if all((y == 0) | (y == 1)):
651
+ binary_y = True
652
+ else:
653
+ binary_y = False
654
+
655
+ loss = np.full(M, np.nan)
656
+ best_model_ind = 0
657
+ best_model = None
658
+
659
+ for i in range(M):
660
+ if i == 0:
661
+ ans = miu_m(
662
+ y=y,
663
+ w=w,
664
+ X=X,
665
+ real_tau=real_tau,
666
+ val_X=val_X,
667
+ val_y=val_y,
668
+ val_w=val_w,
669
+ max_depth=max_depth,
670
+ lambda_=lambda_,
671
+ **tree_args,
672
+ )
673
+ best_model = ans
674
+ else:
675
+ ans = miu_m(
676
+ y=y,
677
+ w=w,
678
+ X=X,
679
+ miu_m_minus_1=ans,
680
+ val_X=val_X,
681
+ val_y=val_y,
682
+ val_w=val_w,
683
+ max_depth=max_depth,
684
+ lambda_=lambda_,
685
+ **tree_args,
686
+ )
687
+
688
+ if val_X is None:
689
+ # Use training data for loss calculation
690
+ pred = np.where(w == 1, ans[1](X), ans[0](X))
691
+ if not binary_y:
692
+ loss[i] = np.mean((y - pred) ** 2)
693
+ else:
694
+ loss[i] = deviance(y, pred)
695
+ else:
696
+ # Use validation data for loss calculation
697
+ pred = np.where(val_w == 1, ans[1](val_X), ans[0](val_X))
698
+ if not binary_y:
699
+ loss[i] = np.mean((val_y - pred) ** 2)
700
+ else:
701
+ loss[i] = deviance(val_y, pred)
702
+
703
+ if np.nanargmin(loss) != best_model_ind:
704
+ best_model_ind = np.nanargmin(loss)
705
+ best_model = ans
706
+ elif i - np.nanargmin(loss) > early_stopping_rounds:
707
+ if verbose:
708
+ print(
709
+ f"Best tree: {best_model_ind + 1}, best tree loss: {loss[best_model_ind]}"
710
+ )
711
+ return {
712
+ "best_model": best_model,
713
+ "loss": loss,
714
+ "best_model_m": best_model_ind + 1,
715
+ }
716
+
717
+ if verbose:
718
+ print(
719
+ f"Best tree: {best_model_ind + 1}, best tree loss: {loss[best_model_ind]}"
720
+ )
721
+ return {"best_model": best_model, "loss": loss, "best_model_m": best_model_ind + 1}
722
+
723
+
724
+ def miu_cv(
725
+ y: ArrayLike,
726
+ w: ArrayLike,
727
+ X: Union[pd.DataFrame, ArrayLike],
728
+ real_tau: float,
729
+ train_frac: float = 0.8,
730
+ val_frac: float = 0.1,
731
+ B: int = 5,
732
+ max_depths: List[int] = [1, 3, 5],
733
+ lambdas: List[float] = np.logspace(
734
+ -5, 1, num=5
735
+ ).tolist(), # range of lambdas is like in glmnet
736
+ M: int = 30,
737
+ early_stopping_rounds: Union[int, float] = float("inf"),
738
+ verbose: bool = False,
739
+ n_jobs: int = -1,
740
+ **tree_args,
741
+ ) -> dict:
742
+ """
743
+ Perform cross-validation to find optimal hyperparameters for the MIU model.
744
+
745
+ This function performs bootstrap-based cross-validation to tune the hyperparameters
746
+ of the MIU algorithm, including max_depth and lambda regularization parameter.
747
+
748
+ Parameters:
749
+ -----------
750
+ y : ArrayLike
751
+ Outcome array. Can be continuous or binary (0/1). Will be converted to np.ndarray.
752
+ w : ArrayLike
753
+ Treatment assignment array (0 for control, 1 for treatment). Will be converted to np.ndarray.
754
+ X : Union[pd.DataFrame, ArrayLike]
755
+ Covariate matrix for training the models. Will be converted to pd.DataFrame.
756
+ real_tau : float
757
+ The true treatment effect to constrain the model.
758
+ train_frac : float, default=0.8
759
+ Fraction of data to use for training in each bootstrap iteration.
760
+ val_frac : float, default=0.1
761
+ Fraction of training data to use for validation. If 0, no validation is performed.
762
+ B : int, default=5
763
+ Number of bootstrap iterations for cross-validation.
764
+ max_depths : List[int], default=[1, 3, 5]
765
+ List of maximum tree depths to try during hyperparameter tuning.
766
+ lambdas : List[float], default=np.logspace(-5, 1, 5).tolist()
767
+ List of L2 regularization parameters to try during hyperparameter tuning.
768
+ Range is similar to glmnet: from 1e-5 to 10.
769
+ M : int, default=30
770
+ Maximum number of ensemble iterations for each model.
771
+ early_stopping_rounds : Union[int, float], default=float('inf')
772
+ Number of rounds without improvement before stopping early.
773
+ If val_frac is 0, this must be float('inf').
774
+ verbose : bool, default=False
775
+ Whether to print progress information during cross-validation.
776
+ n_jobs : int, default=-1
777
+ Number of jobs to run in parallel. -1 means using all processors - 1.
778
+ **tree_args
779
+ Additional arguments passed to DecisionTreeRegressor.
780
+
781
+ Returns:
782
+ --------
783
+ dict
784
+ Dictionary containing:
785
+ - 'final_model': List[Callable] - The best ensemble model trained on full data
786
+ - 'params_loss': pd.DataFrame - Cross-validation results for all parameter combinations
787
+
788
+ Notes:
789
+ ------
790
+ - Uses stratified bootstrap sampling to maintain treatment group proportions
791
+ - Performs parallel processing across parameter combinations and bootstrap iterations
792
+ - Selects best parameters based on mean test loss across bootstrap iterations
793
+ - Final model is trained on the full dataset using the best parameters
794
+ - The params_loss DataFrame contains loss, r_sq, and m for each parameter combination
795
+ """
796
+ # Convert inputs to appropriate types
797
+ y = np.asarray(y)
798
+ w = np.asarray(w)
799
+
800
+ if not isinstance(X, pd.DataFrame):
801
+ X = pd.DataFrame(X)
802
+ # Create parameter grid
803
+ param_combinations = []
804
+ for max_depth in max_depths:
805
+ for lambda_ in lambdas:
806
+ param_combinations.append({"max_depth": max_depth, "lambda_": lambda_})
807
+
808
+ params_loss = pd.DataFrame(param_combinations)
809
+ params_loss["loss"] = np.nan
810
+ params_loss["r_sq"] = np.nan
811
+ params_loss["m"] = np.nan
812
+ params_loss = params_loss.merge(pd.DataFrame({"b": range(B)}), how="cross")
813
+
814
+ # Set number of jobs
815
+ if n_jobs == -1:
816
+ n_jobs = mp.cpu_count() - 1 # don't freeze the computer
817
+
818
+ # Run rows in parallel
819
+ if n_jobs > 1:
820
+ with mp.Pool(processes=n_jobs) as pool:
821
+ params_loss = pool.map(
822
+ partial(
823
+ miu_row,
824
+ y=y,
825
+ w=w,
826
+ X=X,
827
+ real_tau=real_tau,
828
+ train_frac=train_frac,
829
+ val_frac=val_frac,
830
+ M=M,
831
+ early_stopping_rounds=early_stopping_rounds,
832
+ verbose=False,
833
+ **tree_args,
834
+ ),
835
+ [row for _, row in params_loss.iterrows()],
836
+ )
837
+ else:
838
+ params_loss = [
839
+ miu_row(
840
+ row,
841
+ y=y,
842
+ w=w,
843
+ X=X,
844
+ real_tau=real_tau,
845
+ train_frac=train_frac,
846
+ val_frac=val_frac,
847
+ M=M,
848
+ early_stopping_rounds=early_stopping_rounds,
849
+ verbose=False,
850
+ **tree_args,
851
+ )
852
+ for _, row in params_loss.iterrows()
853
+ ]
854
+
855
+ # Aggregate results back into params_loss DataFrame
856
+ params_loss = pd.concat(params_loss, axis=0)
857
+ params_loss = (
858
+ params_loss.groupby(["max_depth", "lambda_"])
859
+ .agg({"loss": "mean", "r_sq": "mean", "m": "mean"})
860
+ .reset_index()
861
+ .assign(m=lambda x: x["m"].astype(int))
862
+ )
863
+
864
+ # Find best parameters
865
+ best_idx = np.argmin(params_loss["loss"])
866
+ params_loss["best_params"] = params_loss.index == best_idx
867
+ best_params = params_loss.iloc[best_idx]
868
+
869
+ if verbose:
870
+ print(
871
+ f"Best params: max_depth - {best_params['max_depth']}, "
872
+ f"lambda - {best_params['lambda_']}, m - {best_params['m']}"
873
+ )
874
+
875
+ # Train final model with best parameters
876
+ final_model = miu(
877
+ y=y,
878
+ w=w,
879
+ X=X,
880
+ real_tau=real_tau,
881
+ val_y=None,
882
+ val_w=None,
883
+ val_X=None,
884
+ max_depth=int(best_params["max_depth"]),
885
+ lambda_=best_params["lambda_"],
886
+ M=int(best_params["m"]),
887
+ early_stopping_rounds=float("inf"),
888
+ verbose=False,
889
+ **tree_args,
890
+ )
891
+
892
+ return {
893
+ "final_model": final_model["best_model"],
894
+ "params_loss": params_loss,
895
+ }
896
+
897
+
898
+ def miu_row(
899
+ row: pd.Series,
900
+ y: ArrayLike,
901
+ w: ArrayLike,
902
+ X: Union[pd.DataFrame, ArrayLike],
903
+ real_tau: float,
904
+ train_frac: float = 0.8,
905
+ val_frac: float = 0.1,
906
+ M: int = 30,
907
+ early_stopping_rounds: Union[int, float] = float("inf"),
908
+ verbose: bool = False,
909
+ **tree_args,
910
+ ) -> pd.Series:
911
+ """
912
+ Train a single MIU model for a specific parameter combination and bootstrap iteration.
913
+
914
+ This function is designed to be used in parallel processing for cross-validation.
915
+ It trains a MIU model with specific hyperparameters on a bootstrap sample and
916
+ evaluates it on the out-of-bag test set.
917
+
918
+ Parameters:
919
+ -----------
920
+ row : pd.Series
921
+ A pandas Series containing the parameter combination to evaluate.
922
+ Must contain 'max_depth' and 'lambda_' keys.
923
+ y : ArrayLike
924
+ Outcome array. Can be continuous or binary (0/1). Will be converted to np.ndarray.
925
+ w : ArrayLike
926
+ Treatment assignment array (0 for control, 1 for treatment). Will be converted to np.ndarray.
927
+ X : Union[pd.DataFrame, ArrayLike]
928
+ Covariate matrix for training the models. Will be converted to pd.DataFrame.
929
+ real_tau : float
930
+ The true treatment effect to constrain the model.
931
+ train_frac : float, default=0.8
932
+ Fraction of data to use for training.
933
+ val_frac : float, default=0.1
934
+ Fraction of training data to use for validation. If 0, no validation is performed.
935
+ M : int, default=30
936
+ Maximum number of ensemble iterations for the model.
937
+ early_stopping_rounds : Union[int, float], default=float('inf')
938
+ Number of rounds without improvement before stopping early.
939
+ If val_frac is 0, this must be float('inf').
940
+ verbose : bool, default=False
941
+ Whether to print progress information during training.
942
+ **tree_args
943
+ Additional arguments passed to DecisionTreeRegressor.
944
+
945
+ Returns:
946
+ --------
947
+ pd.Series
948
+ A pandas Series containing the original parameters plus:
949
+ - 'm': int - The number of iterations in the best model
950
+ - 'loss': float - The test loss (MSE for continuous, deviance for binary)
951
+ - 'r_sq': float - The R-squared value on the test set
952
+
953
+ Notes:
954
+ ------
955
+ - Performs stratified bootstrap sampling to maintain treatment group proportions
956
+ - Uses the parameters from 'row' to train the MIU model
957
+ - Evaluates the model on the out-of-bag test set
958
+ - Returns results as a pandas Series for easy aggregation
959
+ - Designed for parallel processing in cross-validation
960
+ """
961
+ # Convert inputs to appropriate types
962
+ y = np.asarray(y)
963
+ w = np.asarray(w)
964
+
965
+ if not isinstance(X, pd.DataFrame):
966
+ X = pd.DataFrame(X)
967
+
968
+ if all((y == 0) | (y == 1)):
969
+ binary_y = True
970
+ else:
971
+ binary_y = False
972
+
973
+ # Stratified split data into train/test based on treatment w
974
+ n_samples = len(y)
975
+ w_0_indices = np.where(w == 0)[0]
976
+ w_1_indices = np.where(w == 1)[0]
977
+
978
+ # Calculate split sizes for each treatment group
979
+ train_size_0 = int(train_frac * len(w_0_indices))
980
+ train_size_1 = int(train_frac * len(w_1_indices))
981
+
982
+ # Randomly select train indices for each treatment group
983
+ train_indices_0 = np.random.choice(w_0_indices, size=train_size_0, replace=False)
984
+ train_indices_1 = np.random.choice(w_1_indices, size=train_size_1, replace=False)
985
+ train_indices = np.concatenate([train_indices_0, train_indices_1])
986
+
987
+ # Remaining indices go to test
988
+ test_indices = np.setdiff1d(np.arange(n_samples), train_indices)
989
+
990
+ if val_frac > 0:
991
+ # Further stratified split train into train/validation
992
+ val_size_0 = int(val_frac * len(train_indices_0))
993
+ val_size_1 = int(val_frac * len(train_indices_1))
994
+
995
+ val_indices_0 = np.random.choice(
996
+ train_indices_0, size=val_size_0, replace=False
997
+ )
998
+ val_indices_1 = np.random.choice(
999
+ train_indices_1, size=val_size_1, replace=False
1000
+ )
1001
+ val_indices = np.concatenate([val_indices_0, val_indices_1])
1002
+
1003
+ # Remove validation indices from train
1004
+ train_indices = np.setdiff1d(train_indices, val_indices)
1005
+
1006
+ val_X = X.iloc[val_indices]
1007
+ val_y = y[val_indices]
1008
+ val_w = w[val_indices]
1009
+ else:
1010
+ val_X = None
1011
+ val_y = None
1012
+ val_w = None
1013
+
1014
+ train_X = X.iloc[train_indices]
1015
+ train_y = y[train_indices]
1016
+ train_w = w[train_indices]
1017
+ test_X = X.iloc[test_indices]
1018
+ test_y = y[test_indices]
1019
+ test_w = w[test_indices]
1020
+ miu_row = miu(
1021
+ y=train_y,
1022
+ w=train_w,
1023
+ X=train_X,
1024
+ real_tau=real_tau,
1025
+ val_y=val_y,
1026
+ val_w=val_w,
1027
+ val_X=val_X,
1028
+ max_depth=int(row["max_depth"]),
1029
+ lambda_=float(row["lambda_"]),
1030
+ M=M,
1031
+ early_stopping_rounds=early_stopping_rounds,
1032
+ verbose=verbose,
1033
+ **tree_args,
1034
+ )
1035
+
1036
+ # Make predictions on test set
1037
+ pred = np.where(
1038
+ test_w == 1, miu_row["best_model"][1](test_X), miu_row["best_model"][0](test_X)
1039
+ )
1040
+
1041
+ # Create result dict
1042
+ row["m"] = miu_row["best_model_m"]
1043
+
1044
+ if not binary_y:
1045
+ row["loss"] = np.mean((test_y - pred) ** 2)
1046
+ row["r_sq"] = 1 - row["loss"] / np.mean((test_y - np.mean(test_y)) ** 2)
1047
+ else:
1048
+ row["loss"] = deviance(test_y, pred)
1049
+ baseline_pred = np.where(
1050
+ test_w == 1,
1051
+ logit(np.mean(test_y[test_w == 1])),
1052
+ logit(np.mean(test_y[test_w == 0])),
1053
+ )
1054
+ row["r_sq"] = 1 - row["loss"] / deviance(test_y, baseline_pred)
1055
+
1056
+ return row.to_frame().T
causalml/source/causalml/dataset/synthetic.py ADDED
@@ -0,0 +1,655 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from matplotlib import pyplot as plt
2
+ import numpy as np
3
+ import pandas as pd
4
+ from sklearn.metrics import mean_squared_error as mse
5
+ from sklearn.metrics import auc
6
+ from sklearn.model_selection import train_test_split
7
+ from sklearn.linear_model import LinearRegression
8
+ from xgboost import XGBRegressor
9
+ from scipy.stats import entropy
10
+ import warnings
11
+
12
+ from causalml.inference.meta import (
13
+ BaseXRegressor,
14
+ BaseRRegressor,
15
+ BaseSRegressor,
16
+ BaseTRegressor,
17
+ )
18
+ from causalml.inference.tree.causal.causaltree import CausalTreeRegressor
19
+ from causalml.propensity import ElasticNetPropensityModel
20
+ from causalml.metrics import plot_gain, get_cumgain
21
+
22
+ plt.style.use("fivethirtyeight")
23
+ warnings.filterwarnings("ignore")
24
+
25
+ KEY_GENERATED_DATA = "generated_data"
26
+ KEY_ACTUAL = "Actuals"
27
+
28
+ RANDOM_SEED = 42
29
+
30
+
31
+ def get_synthetic_preds(synthetic_data_func, n=1000, estimators={}):
32
+ """Generate predictions for synthetic data using specified function (single simulation)
33
+
34
+ Args:
35
+ synthetic_data_func (function): synthetic data generation function
36
+ n (int, optional): number of samples
37
+ estimators (dict of object): dict of names and objects of treatment effect estimators
38
+
39
+ Returns:
40
+ (dict): dict of the actual and estimates of treatment effects
41
+ """
42
+ y, X, w, tau, b, e = synthetic_data_func(n=n)
43
+
44
+ preds_dict = {}
45
+ preds_dict[KEY_ACTUAL] = tau
46
+ preds_dict[KEY_GENERATED_DATA] = {
47
+ "y": y,
48
+ "X": X,
49
+ "w": w,
50
+ "tau": tau,
51
+ "b": b,
52
+ "e": e,
53
+ }
54
+
55
+ # Predict p_hat because e would not be directly observed in real-life
56
+ p_model = ElasticNetPropensityModel()
57
+ p_hat = p_model.fit_predict(X, w)
58
+
59
+ if estimators:
60
+ for name, learner in estimators.items():
61
+ try:
62
+ preds_dict[name] = learner.fit_predict(
63
+ X=X, treatment=w, y=y, p=p_hat
64
+ ).flatten()
65
+ except TypeError:
66
+ preds_dict[name] = learner.fit_predict(X=X, treatment=w, y=y).flatten()
67
+ else:
68
+ for base_learner, label_l in zip(
69
+ [BaseSRegressor, BaseTRegressor, BaseXRegressor, BaseRRegressor],
70
+ ["S", "T", "X", "R"],
71
+ ):
72
+ for model, label_m in zip([LinearRegression, XGBRegressor], ["LR", "XGB"]):
73
+ learner = base_learner(model())
74
+ model_name = "{} Learner ({})".format(label_l, label_m)
75
+ try:
76
+ preds_dict[model_name] = learner.fit_predict(
77
+ X=X, treatment=w, y=y, p=p_hat
78
+ ).flatten()
79
+ except TypeError:
80
+ preds_dict[model_name] = learner.fit_predict(
81
+ X=X, treatment=w, y=y
82
+ ).flatten()
83
+
84
+ learner = CausalTreeRegressor(random_state=RANDOM_SEED)
85
+ preds_dict["Causal Tree"] = learner.fit_predict(X=X, treatment=w, y=y).flatten()
86
+
87
+ return preds_dict
88
+
89
+
90
+ def get_synthetic_summary(synthetic_data_func, n=1000, k=1, estimators={}):
91
+ """Generate a summary for predictions on synthetic data using specified function
92
+
93
+ Args:
94
+ synthetic_data_func (function): synthetic data generation function
95
+ n (int, optional): number of samples per simulation
96
+ k (int, optional): number of simulations
97
+ """
98
+ summaries = []
99
+
100
+ for i in range(k):
101
+ synthetic_preds = get_synthetic_preds(
102
+ synthetic_data_func, n=n, estimators=estimators
103
+ )
104
+ actuals = synthetic_preds[KEY_ACTUAL]
105
+ synthetic_summary = pd.DataFrame(
106
+ {
107
+ label: [preds.mean(), mse(preds, actuals)]
108
+ for label, preds in synthetic_preds.items()
109
+ if label != KEY_GENERATED_DATA
110
+ },
111
+ index=["ATE", "MSE"],
112
+ ).T
113
+
114
+ synthetic_summary["Abs % Error of ATE"] = np.abs(
115
+ (synthetic_summary["ATE"] / synthetic_summary.loc[KEY_ACTUAL, "ATE"]) - 1
116
+ )
117
+
118
+ for label in synthetic_summary.index:
119
+ stacked_values = np.hstack((synthetic_preds[label], actuals))
120
+ stacked_low = np.percentile(stacked_values, 0.1)
121
+ stacked_high = np.percentile(stacked_values, 99.9)
122
+ bins = np.linspace(stacked_low, stacked_high, 100)
123
+
124
+ distr = np.histogram(synthetic_preds[label], bins=bins)[0]
125
+ distr = np.clip(distr / distr.sum(), 0.001, 0.999)
126
+ true_distr = np.histogram(actuals, bins=bins)[0]
127
+ true_distr = np.clip(true_distr / true_distr.sum(), 0.001, 0.999)
128
+
129
+ kl = entropy(distr, true_distr)
130
+ synthetic_summary.loc[label, "KL Divergence"] = kl
131
+
132
+ summaries.append(synthetic_summary)
133
+
134
+ summary = sum(summaries) / k
135
+ return summary[["Abs % Error of ATE", "MSE", "KL Divergence"]]
136
+
137
+
138
+ def scatter_plot_summary(synthetic_summary, k, drop_learners=[], drop_cols=[]):
139
+ """Generates a scatter plot comparing learner performance. Each learner's performance is plotted as a point in the
140
+ (Abs % Error of ATE, MSE) space.
141
+
142
+ Args:
143
+ synthetic_summary (pd.DataFrame): summary generated by get_synthetic_summary()
144
+ k (int): number of simulations (used only for plot title text)
145
+ drop_learners (list, optional): list of learners (str) to omit when plotting
146
+ drop_cols (list, optional): list of metrics (str) to omit when plotting
147
+ """
148
+ plot_data = synthetic_summary.drop(drop_learners).drop(drop_cols, axis=1)
149
+
150
+ fig, ax = plt.subplots()
151
+ fig.set_size_inches(12, 8)
152
+ xs = plot_data["Abs % Error of ATE"]
153
+ ys = plot_data["MSE"]
154
+
155
+ ax.scatter(xs, ys)
156
+
157
+ ylim = ax.get_ylim()
158
+ xlim = ax.get_xlim()
159
+
160
+ for i, txt in enumerate(plot_data.index):
161
+ ax.annotate(
162
+ txt,
163
+ (
164
+ xs[i] - np.random.binomial(1, 0.5) * xlim[1] * 0.04,
165
+ ys[i] - ylim[1] * 0.03,
166
+ ),
167
+ )
168
+
169
+ ax.set_xlabel("Abs % Error of ATE")
170
+ ax.set_ylabel("MSE")
171
+ ax.set_title("Learner Performance (averaged over k={} simulations)".format(k))
172
+
173
+
174
+ def bar_plot_summary(
175
+ synthetic_summary,
176
+ k,
177
+ drop_learners=[],
178
+ drop_cols=[],
179
+ sort_cols=["MSE", "Abs % Error of ATE"],
180
+ ):
181
+ """Generates a bar plot comparing learner performance.
182
+
183
+ Args:
184
+ synthetic_summary (pd.DataFrame): summary generated by get_synthetic_summary()
185
+ k (int): number of simulations (used only for plot title text)
186
+ drop_learners (list, optional): list of learners (str) to omit when plotting
187
+ drop_cols (list, optional): list of metrics (str) to omit when plotting
188
+ sort_cols (list, optional): list of metrics (str) to sort on when plotting
189
+ """
190
+ plot_data = synthetic_summary.sort_values(sort_cols, ascending=True)
191
+ plot_data = plot_data.drop(drop_learners + [KEY_ACTUAL]).drop(drop_cols, axis=1)
192
+
193
+ plot_data.plot(kind="bar", figsize=(12, 8))
194
+ plt.xticks(rotation=30)
195
+ plt.title("Learner Performance (averaged over k={} simulations)".format(k))
196
+
197
+
198
+ def distr_plot_single_sim(
199
+ synthetic_preds,
200
+ kind="kde",
201
+ drop_learners=[],
202
+ bins=50,
203
+ histtype="step",
204
+ alpha=1,
205
+ linewidth=1,
206
+ bw_method=1,
207
+ ):
208
+ """Plots the distribution of each learner's predictions (for a single simulation).
209
+ Kernel Density Estimation (kde) and actual histogram plots supported.
210
+
211
+ Args:
212
+ synthetic_preds (dict): dictionary of predictions generated by get_synthetic_preds()
213
+ kind (str, optional): 'kde' or 'hist'
214
+ drop_learners (list, optional): list of learners (str) to omit when plotting
215
+ bins (int, optional): number of bins to plot if kind set to 'hist'
216
+ histtype (str, optional): histogram type if kind set to 'hist'
217
+ alpha (float, optional): alpha (transparency) for plotting
218
+ linewidth (int, optional): line width for plotting
219
+ bw_method (float, optional): parameter for kde
220
+ """
221
+ preds_for_plot = synthetic_preds.copy()
222
+
223
+ # deleted generated data and assign actual value
224
+ del preds_for_plot[KEY_GENERATED_DATA]
225
+ global_lower = np.percentile(np.hstack(list(preds_for_plot.values())), 1)
226
+ global_upper = np.percentile(np.hstack(list(preds_for_plot.values())), 99)
227
+ learners = list(preds_for_plot.keys())
228
+ learners = [learner for learner in learners if learner not in drop_learners]
229
+
230
+ # Plotting
231
+ plt.figure(figsize=(12, 8))
232
+ colors = [
233
+ "black",
234
+ "red",
235
+ "blue",
236
+ "green",
237
+ "cyan",
238
+ "brown",
239
+ "grey",
240
+ "pink",
241
+ "orange",
242
+ "yellow",
243
+ ]
244
+ for i, (k, v) in enumerate(preds_for_plot.items()):
245
+ if k in learners:
246
+ if kind == "kde":
247
+ v = pd.Series(v.flatten())
248
+ v = v[v.between(global_lower, global_upper)]
249
+ v.plot(
250
+ kind="kde",
251
+ bw_method=bw_method,
252
+ label=k,
253
+ linewidth=linewidth,
254
+ color=colors[i],
255
+ )
256
+ elif kind == "hist":
257
+ plt.hist(
258
+ v,
259
+ bins=np.linspace(global_lower, global_upper, bins),
260
+ label=k,
261
+ histtype=histtype,
262
+ alpha=alpha,
263
+ linewidth=linewidth,
264
+ color=colors[i],
265
+ )
266
+ else:
267
+ pass
268
+
269
+ plt.xlim(global_lower, global_upper)
270
+ plt.legend(loc="center left", bbox_to_anchor=(1, 0.5))
271
+ plt.title("Distribution from a Single Simulation")
272
+
273
+
274
+ def scatter_plot_single_sim(synthetic_preds):
275
+ """Creates a grid of scatter plots comparing each learner's predictions with the truth (for a single simulation).
276
+
277
+ Args:
278
+ synthetic_preds (dict): dictionary of predictions generated by get_synthetic_preds() or
279
+ get_synthetic_preds_holdout()
280
+ """
281
+ preds_for_plot = synthetic_preds.copy()
282
+
283
+ # deleted generated data and get actual column name
284
+ del preds_for_plot[KEY_GENERATED_DATA]
285
+ n_row = int(np.ceil(len(preds_for_plot.keys()) / 3))
286
+
287
+ fig, axes = plt.subplots(n_row, 3, figsize=(5 * n_row, 15))
288
+ axes = np.ravel(axes)
289
+
290
+ for i, (label, preds) in enumerate(preds_for_plot.items()):
291
+ axes[i].scatter(preds_for_plot[KEY_ACTUAL], preds, s=2, label="Predictions")
292
+ axes[i].set_title(label, size=12)
293
+ axes[i].set_xlabel("Actual", size=10)
294
+ axes[i].set_ylabel("Prediction", size=10)
295
+ xlim = axes[i].get_xlim()
296
+ ylim = axes[i].get_xlim()
297
+ axes[i].plot(
298
+ [xlim[0], xlim[1]],
299
+ [ylim[0], ylim[1]],
300
+ label="Perfect Model",
301
+ linewidth=1,
302
+ color="grey",
303
+ )
304
+ axes[i].legend(loc=2, prop={"size": 10})
305
+
306
+
307
+ def get_synthetic_preds_holdout(
308
+ synthetic_data_func, n=1000, valid_size=0.2, estimators={}
309
+ ):
310
+ """Generate predictions for synthetic data using specified function (single simulation) for train and holdout
311
+
312
+ Args:
313
+ synthetic_data_func (function): synthetic data generation function
314
+ n (int, optional): number of samples
315
+ valid_size(float,optional): validaiton/hold out data size
316
+ estimators (dict of object): dict of names and objects of treatment effect estimators
317
+
318
+ Returns:
319
+ (tuple): synthetic training and validation data dictionaries:
320
+
321
+ - preds_dict_train (dict): synthetic training data dictionary
322
+ - preds_dict_valid (dict): synthetic validation data dictionary
323
+ """
324
+ y, X, w, tau, b, e = synthetic_data_func(n=n)
325
+
326
+ (
327
+ X_train,
328
+ X_val,
329
+ y_train,
330
+ y_val,
331
+ w_train,
332
+ w_val,
333
+ tau_train,
334
+ tau_val,
335
+ b_train,
336
+ b_val,
337
+ e_train,
338
+ e_val,
339
+ ) = train_test_split(
340
+ X, y, w, tau, b, e, test_size=valid_size, random_state=RANDOM_SEED, shuffle=True
341
+ )
342
+
343
+ preds_dict_train = {}
344
+ preds_dict_valid = {}
345
+
346
+ preds_dict_train[KEY_ACTUAL] = tau_train
347
+ preds_dict_valid[KEY_ACTUAL] = tau_val
348
+
349
+ preds_dict_train["generated_data"] = {
350
+ "y": y_train,
351
+ "X": X_train,
352
+ "w": w_train,
353
+ "tau": tau_train,
354
+ "b": b_train,
355
+ "e": e_train,
356
+ }
357
+ preds_dict_valid["generated_data"] = {
358
+ "y": y_val,
359
+ "X": X_val,
360
+ "w": w_val,
361
+ "tau": tau_val,
362
+ "b": b_val,
363
+ "e": e_val,
364
+ }
365
+
366
+ # Predict p_hat because e would not be directly observed in real-life
367
+ p_model = ElasticNetPropensityModel()
368
+ p_hat_train = p_model.fit_predict(X_train, w_train)
369
+ p_hat_val = p_model.fit_predict(X_val, w_val)
370
+
371
+ for base_learner, label_l in zip(
372
+ [BaseSRegressor, BaseTRegressor, BaseXRegressor, BaseRRegressor],
373
+ ["S", "T", "X", "R"],
374
+ ):
375
+ for model, label_m in zip([LinearRegression, XGBRegressor], ["LR", "XGB"]):
376
+ # RLearner will need to fit on the p_hat
377
+ if label_l != "R":
378
+ learner = base_learner(model())
379
+ # fit the model on training data only
380
+ learner.fit(X=X_train, treatment=w_train, y=y_train)
381
+ try:
382
+ preds_dict_train["{} Learner ({})".format(label_l, label_m)] = (
383
+ learner.predict(X=X_train, p=p_hat_train).flatten()
384
+ )
385
+ preds_dict_valid["{} Learner ({})".format(label_l, label_m)] = (
386
+ learner.predict(X=X_val, p=p_hat_val).flatten()
387
+ )
388
+ except TypeError:
389
+ preds_dict_train["{} Learner ({})".format(label_l, label_m)] = (
390
+ learner.predict(
391
+ X=X_train, treatment=w_train, y=y_train
392
+ ).flatten()
393
+ )
394
+ preds_dict_valid["{} Learner ({})".format(label_l, label_m)] = (
395
+ learner.predict(X=X_val, treatment=w_val, y=y_val).flatten()
396
+ )
397
+ else:
398
+ learner = base_learner(model())
399
+ learner.fit(X=X_train, p=p_hat_train, treatment=w_train, y=y_train)
400
+ preds_dict_train["{} Learner ({})".format(label_l, label_m)] = (
401
+ learner.predict(X=X_train).flatten()
402
+ )
403
+ preds_dict_valid["{} Learner ({})".format(label_l, label_m)] = (
404
+ learner.predict(X=X_val).flatten()
405
+ )
406
+
407
+ return preds_dict_train, preds_dict_valid
408
+
409
+
410
+ def get_synthetic_summary_holdout(synthetic_data_func, n=1000, valid_size=0.2, k=1):
411
+ """Generate a summary for predictions on synthetic data for train and holdout using specified function
412
+
413
+ Args:
414
+ synthetic_data_func (function): synthetic data generation function
415
+ n (int, optional): number of samples per simulation
416
+ valid_size(float,optional): validation/hold out data size
417
+ k (int, optional): number of simulations
418
+
419
+
420
+ Returns:
421
+ (tuple): summary evaluation metrics of predictions for train and validation:
422
+
423
+ - summary_train (pandas.DataFrame): training data evaluation summary
424
+ - summary_train (pandas.DataFrame): validation data evaluation summary
425
+ """
426
+
427
+ summaries_train = []
428
+ summaries_validation = []
429
+
430
+ for i in range(k):
431
+ preds_dict_train, preds_dict_valid = get_synthetic_preds_holdout(
432
+ synthetic_data_func, n=n, valid_size=valid_size
433
+ )
434
+ actuals_train = preds_dict_train[KEY_ACTUAL]
435
+ actuals_validation = preds_dict_valid[KEY_ACTUAL]
436
+
437
+ synthetic_summary_train = pd.DataFrame(
438
+ {
439
+ label: [preds.mean(), mse(preds, actuals_train)]
440
+ for label, preds in preds_dict_train.items()
441
+ if KEY_GENERATED_DATA not in label.lower()
442
+ },
443
+ index=["ATE", "MSE"],
444
+ ).T
445
+ synthetic_summary_train["Abs % Error of ATE"] = np.abs(
446
+ (
447
+ synthetic_summary_train["ATE"]
448
+ / synthetic_summary_train.loc[KEY_ACTUAL, "ATE"]
449
+ )
450
+ - 1
451
+ )
452
+
453
+ synthetic_summary_validation = pd.DataFrame(
454
+ {
455
+ label: [preds.mean(), mse(preds, actuals_validation)]
456
+ for label, preds in preds_dict_valid.items()
457
+ if KEY_GENERATED_DATA not in label.lower()
458
+ },
459
+ index=["ATE", "MSE"],
460
+ ).T
461
+ synthetic_summary_validation["Abs % Error of ATE"] = np.abs(
462
+ (
463
+ synthetic_summary_validation["ATE"]
464
+ / synthetic_summary_validation.loc[KEY_ACTUAL, "ATE"]
465
+ )
466
+ - 1
467
+ )
468
+
469
+ # calculate kl divergence for training
470
+ for label in synthetic_summary_train.index:
471
+ stacked_values = np.hstack((preds_dict_train[label], actuals_train))
472
+ stacked_low = np.percentile(stacked_values, 0.1)
473
+ stacked_high = np.percentile(stacked_values, 99.9)
474
+ bins = np.linspace(stacked_low, stacked_high, 100)
475
+
476
+ distr = np.histogram(preds_dict_train[label], bins=bins)[0]
477
+ distr = np.clip(distr / distr.sum(), 0.001, 0.999)
478
+ true_distr = np.histogram(actuals_train, bins=bins)[0]
479
+ true_distr = np.clip(true_distr / true_distr.sum(), 0.001, 0.999)
480
+
481
+ kl = entropy(distr, true_distr)
482
+ synthetic_summary_train.loc[label, "KL Divergence"] = kl
483
+
484
+ # calculate kl divergence for validation
485
+ for label in synthetic_summary_validation.index:
486
+ stacked_values = np.hstack((preds_dict_valid[label], actuals_validation))
487
+ stacked_low = np.percentile(stacked_values, 0.1)
488
+ stacked_high = np.percentile(stacked_values, 99.9)
489
+ bins = np.linspace(stacked_low, stacked_high, 100)
490
+
491
+ distr = np.histogram(preds_dict_valid[label], bins=bins)[0]
492
+ distr = np.clip(distr / distr.sum(), 0.001, 0.999)
493
+ true_distr = np.histogram(actuals_validation, bins=bins)[0]
494
+ true_distr = np.clip(true_distr / true_distr.sum(), 0.001, 0.999)
495
+
496
+ kl = entropy(distr, true_distr)
497
+ synthetic_summary_validation.loc[label, "KL Divergence"] = kl
498
+
499
+ summaries_train.append(synthetic_summary_train)
500
+ summaries_validation.append(synthetic_summary_validation)
501
+
502
+ summary_train = sum(summaries_train) / k
503
+ summary_validation = sum(summaries_validation) / k
504
+ return (
505
+ summary_train[["Abs % Error of ATE", "MSE", "KL Divergence"]],
506
+ summary_validation[["Abs % Error of ATE", "MSE", "KL Divergence"]],
507
+ )
508
+
509
+
510
+ def scatter_plot_summary_holdout(
511
+ train_summary,
512
+ validation_summary,
513
+ k,
514
+ label=["Train", "Validation"],
515
+ drop_learners=[],
516
+ drop_cols=[],
517
+ ):
518
+ """Generates a scatter plot comparing learner performance by training and validation.
519
+
520
+ Args:
521
+ train_summary (pd.DataFrame): summary for training synthetic data generated by get_synthetic_summary_holdout()
522
+ validation_summary (pd.DataFrame): summary for validation synthetic data generated by
523
+ get_synthetic_summary_holdout()
524
+ label (string, optional): legend label for plot
525
+ k (int): number of simulations (used only for plot title text)
526
+ drop_learners (list, optional): list of learners (str) to omit when plotting
527
+ drop_cols (list, optional): list of metrics (str) to omit when plotting
528
+ """
529
+ train_summary = train_summary.drop(drop_learners).drop(drop_cols, axis=1)
530
+ validation_summary = validation_summary.drop(drop_learners).drop(drop_cols, axis=1)
531
+
532
+ plot_data = pd.concat([train_summary, validation_summary])
533
+ plot_data["label"] = [i.replace("Train", "") for i in plot_data.index]
534
+ plot_data["label"] = [i.replace("Validation", "") for i in plot_data.label]
535
+
536
+ fig, ax = plt.subplots()
537
+ fig.set_size_inches(12, 8)
538
+ xs = plot_data["Abs % Error of ATE"]
539
+ ys = plot_data["MSE"]
540
+ group = np.array(
541
+ [label[0]] * train_summary.shape[0] + [label[1]] * validation_summary.shape[0]
542
+ )
543
+ cdict = {label[0]: "red", label[1]: "blue"}
544
+
545
+ for g in np.unique(group):
546
+ ix = np.where(group == g)[0].tolist()
547
+ ax.scatter(xs[ix], ys[ix], c=cdict[g], label=g, s=100)
548
+
549
+ for i, txt in enumerate(plot_data.label[:10]):
550
+ ax.annotate(txt, (xs[i] + 0.005, ys[i]))
551
+
552
+ ax.set_xlabel("Abs % Error of ATE")
553
+ ax.set_ylabel("MSE")
554
+ ax.set_title("Learner Performance (averaged over k={} simulations)".format(k))
555
+ ax.legend(loc="center left", bbox_to_anchor=(1.1, 0.5))
556
+ plt.show()
557
+
558
+
559
+ def bar_plot_summary_holdout(
560
+ train_summary, validation_summary, k, drop_learners=[], drop_cols=[]
561
+ ):
562
+ """Generates a bar plot comparing learner performance by training and validation
563
+
564
+ Args:
565
+ train_summary (pd.DataFrame): summary for training synthetic data generated by get_synthetic_summary_holdout()
566
+ validation_summary (pd.DataFrame): summary for validation synthetic data generated by
567
+ get_synthetic_summary_holdout()
568
+ k (int): number of simulations (used only for plot title text)
569
+ drop_learners (list, optional): list of learners (str) to omit when plotting
570
+ drop_cols (list, optional): list of metrics (str) to omit when plotting
571
+ """
572
+ train_summary = train_summary.drop([KEY_ACTUAL])
573
+ train_summary["Learner"] = train_summary.index
574
+
575
+ validation_summary = validation_summary.drop([KEY_ACTUAL])
576
+ validation_summary["Learner"] = validation_summary.index
577
+
578
+ for metric in ["Abs % Error of ATE", "MSE", "KL Divergence"]:
579
+ plot_data_sub = pd.DataFrame(train_summary.Learner).reset_index(drop=True)
580
+ plot_data_sub["train"] = train_summary[metric].values
581
+ plot_data_sub["validation"] = validation_summary[metric].values
582
+ plot_data_sub = plot_data_sub.set_index("Learner")
583
+ plot_data_sub = plot_data_sub.drop(drop_learners).drop(drop_cols, axis=1)
584
+ plot_data_sub = plot_data_sub.sort_values("train", ascending=True)
585
+
586
+ plot_data_sub.plot(kind="bar", color=["red", "blue"], figsize=(12, 8))
587
+ plt.xticks(rotation=30)
588
+ plt.title(
589
+ "Learner Performance of {} (averaged over k={} simulations)".format(
590
+ metric, k
591
+ )
592
+ )
593
+
594
+
595
+ def get_synthetic_auuc(
596
+ synthetic_preds,
597
+ drop_learners=[],
598
+ outcome_col="y",
599
+ treatment_col="w",
600
+ treatment_effect_col="tau",
601
+ plot=True,
602
+ ):
603
+ """Get auuc values for cumulative gains of model estimates in quantiles.
604
+
605
+ For details, reference get_cumgain() and plot_gain()
606
+ Args:
607
+ synthetic_preds (dict): dictionary of predictions generated by get_synthetic_preds()
608
+ or get_synthetic_preds_holdout()
609
+ outcome_col (str, optional): the column name for the actual outcome
610
+ treatment_col (str, optional): the column name for the treatment indicator (0 or 1)
611
+ treatment_effect_col (str, optional): the column name for the true treatment effect
612
+ plot (boolean,optional): plot the cumulative gain chart or not
613
+
614
+ Returns:
615
+ (pandas.DataFrame): auuc values by learner for cumulative gains of model estimates
616
+ """
617
+ synthetic_preds_df = synthetic_preds.copy()
618
+ generated_data = synthetic_preds_df.pop(KEY_GENERATED_DATA)
619
+ synthetic_preds_df = pd.DataFrame(synthetic_preds_df)
620
+ synthetic_preds_df = synthetic_preds_df.drop(drop_learners, axis=1)
621
+
622
+ synthetic_preds_df["y"] = generated_data[outcome_col]
623
+ synthetic_preds_df["w"] = generated_data[treatment_col]
624
+ if treatment_effect_col in generated_data.keys():
625
+ synthetic_preds_df["tau"] = generated_data[treatment_effect_col]
626
+
627
+ assert (
628
+ (outcome_col in synthetic_preds_df.columns)
629
+ and (treatment_col in synthetic_preds_df.columns)
630
+ or treatment_effect_col in synthetic_preds_df.columns
631
+ )
632
+
633
+ cumlift = get_cumgain(
634
+ synthetic_preds_df,
635
+ outcome_col="y",
636
+ treatment_col="w",
637
+ treatment_effect_col="tau",
638
+ )
639
+ auuc_df = pd.DataFrame(cumlift.columns)
640
+ auuc_df.columns = ["Learner"]
641
+ auuc_df["cum_gain_auuc"] = [
642
+ auc(cumlift.index.values / 100, cumlift[learner].values)
643
+ for learner in cumlift.columns
644
+ ]
645
+ auuc_df = auuc_df.sort_values("cum_gain_auuc", ascending=False)
646
+
647
+ if plot:
648
+ plot_gain(
649
+ synthetic_preds_df,
650
+ outcome_col=outcome_col,
651
+ treatment_col=treatment_col,
652
+ treatment_effect_col=treatment_effect_col,
653
+ )
654
+
655
+ return auuc_df
causalml/source/causalml/feature_selection/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ from .filters import FilterSelect
causalml/source/causalml/feature_selection/filters.py ADDED
@@ -0,0 +1,663 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Filter feature selection methods for uplift modeling
3
+
4
+ - Currently only for classification problem: the outcome variable of uplift model is binary.
5
+ """
6
+
7
+ import numpy as np
8
+ import pandas as pd
9
+ import statsmodels.api as sm
10
+ from scipy import stats
11
+ from sklearn.impute import SimpleImputer
12
+
13
+
14
+ class FilterSelect:
15
+ """A class for feature importance methods."""
16
+
17
+ def __init__(self):
18
+ return
19
+
20
+ @staticmethod
21
+ def _filter_F_one_feature(data, treatment_indicator, feature_name, y_name, order=1):
22
+ """
23
+ Conduct F-test of the interaction between treatment and one feature.
24
+
25
+ Args:
26
+ data (pd.Dataframe): DataFrame containing outcome, features, and experiment group
27
+ treatment_indicator (string): the column name for binary indicator of treatment (1) or control (0)
28
+ feature_name (string): feature name, as one column in the data DataFrame
29
+ y_name (string): name of the outcome variable
30
+ order (int): the order of feature to be evaluated with the treatment effect, order takes 3 values: 1,2,3.
31
+ order = 1 corresponds to linear importance of the feature, order=2 corresponds to quadratic and linear
32
+ importance of the feature,
33
+ order= 3 will calculate feature importance up to cubic forms.
34
+
35
+ Returns:
36
+ F_test_result : pd.DataFrame
37
+ a data frame containing the feature importance statistics
38
+ """
39
+ Y = data[y_name]
40
+ X = data[[treatment_indicator, feature_name]]
41
+ X = sm.add_constant(X)
42
+ X["{}-{}".format(treatment_indicator, feature_name)] = X[
43
+ [treatment_indicator, feature_name]
44
+ ].product(axis=1)
45
+
46
+ if order not in [1, 2, 3]:
47
+ raise Exception("ValueError: order argument only takes value 1,2,3.")
48
+
49
+ if order == 1:
50
+ pass
51
+ elif order == 2:
52
+ x_tmp_name = "{}_o{}".format(feature_name, order)
53
+ X[x_tmp_name] = X[[feature_name]] ** order
54
+ X["{}-{}".format(treatment_indicator, x_tmp_name)] = X[
55
+ [treatment_indicator, x_tmp_name]
56
+ ].product(axis=1)
57
+ elif order == 3:
58
+ x_tmp_name = "{}_o{}".format(feature_name, 2)
59
+ X[x_tmp_name] = X[[feature_name]] ** 2
60
+ X["{}-{}".format(treatment_indicator, x_tmp_name)] = X[
61
+ [treatment_indicator, x_tmp_name]
62
+ ].product(axis=1)
63
+
64
+ x_tmp_name = "{}_o{}".format(feature_name, order)
65
+ X[x_tmp_name] = X[[feature_name]] ** order
66
+ X["{}-{}".format(treatment_indicator, x_tmp_name)] = X[
67
+ [treatment_indicator, x_tmp_name]
68
+ ].product(axis=1)
69
+
70
+ model = sm.OLS(Y, X)
71
+ result = model.fit()
72
+
73
+ if order == 1:
74
+ F_test = result.f_test(np.array([0, 0, 0, 1]))
75
+ elif order == 2:
76
+ F_test = result.f_test(np.array([[0, 0, 0, 1, 0, 0], [0, 0, 0, 0, 0, 1]]))
77
+ elif order == 3:
78
+ F_test = result.f_test(
79
+ np.array(
80
+ [
81
+ [0, 0, 0, 1, 0, 0, 0, 0],
82
+ [0, 0, 0, 0, 0, 1, 0, 0],
83
+ [0, 0, 0, 0, 0, 0, 0, 1],
84
+ ]
85
+ )
86
+ )
87
+
88
+ F_test_result = pd.DataFrame(
89
+ {
90
+ "feature": feature_name, # for the interaction, not the main effect
91
+ "method": "F{} Filter".format(order),
92
+ "score": float(F_test.fvalue),
93
+ "p_value": F_test.pvalue,
94
+ "misc": "df_num: {}, df_denom: {}, order:{}".format(
95
+ F_test.df_num, F_test.df_denom, order
96
+ ),
97
+ },
98
+ index=[0],
99
+ ).reset_index(drop=True)
100
+
101
+ return F_test_result
102
+
103
+ def filter_F(self, data, treatment_indicator, features, y_name, order=1):
104
+ """
105
+ Rank features based on the F-statistics of the interaction.
106
+
107
+ Args:
108
+ data (pd.Dataframe): DataFrame containing outcome, features, and experiment group
109
+ treatment_indicator (string): the column name for binary indicator of treatment (1) or control (0)
110
+ features (list of string): list of feature names, that are columns in the data DataFrame
111
+ y_name (string): name of the outcome variable
112
+ order (int): the order of feature to be evaluated with the treatment effect, order takes 3 values: 1,2,3.
113
+ order = 1 corresponds to linear importance of the feature, order=2 corresponds to quadratic and linear
114
+ importance of the feature,
115
+ order= 3 will calculate feature importance up to cubic forms.
116
+
117
+ Returns:
118
+ all_result : pd.DataFrame
119
+ a data frame containing the feature importance statistics
120
+ """
121
+ if order not in [1, 2, 3]:
122
+ raise Exception("ValueError: order argument only takes value 1,2,3.")
123
+
124
+ all_result = pd.DataFrame()
125
+ for x_name_i in features:
126
+ one_result = self._filter_F_one_feature(
127
+ data=data,
128
+ treatment_indicator=treatment_indicator,
129
+ feature_name=x_name_i,
130
+ y_name=y_name,
131
+ order=order,
132
+ )
133
+ all_result = pd.concat([all_result, one_result])
134
+
135
+ all_result = all_result.sort_values(by="score", ascending=False)
136
+ all_result["rank"] = all_result["score"].rank(ascending=False)
137
+
138
+ return all_result
139
+
140
+ @staticmethod
141
+ def _filter_LR_one_feature(
142
+ data, treatment_indicator, feature_name, y_name, order=1, disp=True
143
+ ):
144
+ """
145
+ Conduct LR (Likelihood Ratio) test of the interaction between treatment and one feature.
146
+
147
+ Args:
148
+ data (pd.Dataframe): DataFrame containing outcome, features, and experiment group
149
+ treatment_indicator (string): the column name for binary indicator of treatment (1) or control (0)
150
+ feature_name (string): feature name, as one column in the data DataFrame
151
+ y_name (string): name of the outcome variable
152
+ order (int): the order of feature to be evaluated with the treatment effect, order takes 3 values: 1,2,3.
153
+ order = 1 corresponds to linear importance of the feature, order=2 corresponds to quadratic and linear
154
+ importance of the feature,
155
+ order= 3 will calculate feature importance up to cubic forms.
156
+
157
+ Returns:
158
+ LR_test_result : pd.DataFrame
159
+ a data frame containing the feature importance statistics
160
+ """
161
+ Y = data[y_name]
162
+
163
+ # Restricted model
164
+ x_name_r = ["const", treatment_indicator, feature_name]
165
+ x_name_f = x_name_r.copy()
166
+ X = data[[treatment_indicator, feature_name]]
167
+ X = sm.add_constant(X)
168
+
169
+ X["{}-{}".format(treatment_indicator, feature_name)] = X[
170
+ [treatment_indicator, feature_name]
171
+ ].product(axis=1)
172
+ x_name_f.append("{}-{}".format(treatment_indicator, feature_name))
173
+
174
+ if order == 2:
175
+ x_tmp_name = "{}_o{}".format(feature_name, order)
176
+ X[x_tmp_name] = X[[feature_name]] ** order
177
+ X["{}-{}".format(treatment_indicator, x_tmp_name)] = X[
178
+ [treatment_indicator, x_tmp_name]
179
+ ].product(axis=1)
180
+ x_name_r.append(x_tmp_name)
181
+ x_name_f += [x_tmp_name, "{}-{}".format(treatment_indicator, x_tmp_name)]
182
+ elif order == 3:
183
+ x_tmp_name = "{}_o{}".format(feature_name, 2)
184
+ X[x_tmp_name] = X[[feature_name]] ** 2
185
+ X["{}-{}".format(treatment_indicator, x_tmp_name)] = X[
186
+ [treatment_indicator, x_tmp_name]
187
+ ].product(axis=1)
188
+ x_name_r.append(x_tmp_name)
189
+ x_name_f += [x_tmp_name, "{}-{}".format(treatment_indicator, x_tmp_name)]
190
+ x_tmp_name = "{}_o{}".format(feature_name, order)
191
+ X[x_tmp_name] = X[[feature_name]] ** order
192
+ X["{}-{}".format(treatment_indicator, x_tmp_name)] = X[
193
+ [treatment_indicator, x_tmp_name]
194
+ ].product(axis=1)
195
+ x_name_r.append(x_tmp_name)
196
+ x_name_f += [x_tmp_name, "{}-{}".format(treatment_indicator, x_tmp_name)]
197
+
198
+ # Full model (with interaction)
199
+ model_r = sm.Logit(Y, X[x_name_r])
200
+ result_r = model_r.fit(disp=disp)
201
+
202
+ model_f = sm.Logit(Y, X[x_name_f])
203
+ result_f = model_f.fit(disp=disp)
204
+
205
+ LR_stat = -2 * (result_r.llf - result_f.llf)
206
+ LR_df = len(result_f.params) - len(result_r.params)
207
+ LR_pvalue = 1 - stats.chi2.cdf(LR_stat, df=LR_df)
208
+
209
+ LR_test_result = pd.DataFrame(
210
+ {
211
+ "feature": feature_name, # for the interaction, not the main effect
212
+ "method": "LR{} Filter".format(order),
213
+ "score": LR_stat,
214
+ "p_value": LR_pvalue,
215
+ "misc": "df: {}, order: {}".format(LR_df, order),
216
+ },
217
+ index=[0],
218
+ ).reset_index(drop=True)
219
+
220
+ return LR_test_result
221
+
222
+ def filter_LR(
223
+ self, data, treatment_indicator, features, y_name, order=1, disp=True
224
+ ):
225
+ """
226
+ Rank features based on the LRT-statistics of the interaction.
227
+
228
+ Args:
229
+ data (pd.Dataframe): DataFrame containing outcome, features, and experiment group
230
+ treatment_indicator (string): the column name for binary indicator of treatment (1) or control (0)
231
+ feature_name (string): feature name, as one column in the data DataFrame
232
+ y_name (string): name of the outcome variable
233
+ order (int): the order of feature to be evaluated with the treatment effect, order takes 3 values: 1,2,3.
234
+ order = 1 corresponds to linear importance of the feature, order=2 corresponds to quadratic and linear
235
+ importance of the feature,
236
+ order= 3 will calculate feature importance up to cubic forms.
237
+
238
+ Returns:
239
+ all_result : pd.DataFrame
240
+ a data frame containing the feature importance statistics
241
+ """
242
+ if order not in [1, 2, 3]:
243
+ raise Exception("ValueError: order argument only takes value 1,2,3.")
244
+
245
+ all_result = pd.DataFrame()
246
+ for x_name_i in features:
247
+ one_result = self._filter_LR_one_feature(
248
+ data=data,
249
+ treatment_indicator=treatment_indicator,
250
+ feature_name=x_name_i,
251
+ y_name=y_name,
252
+ order=order,
253
+ disp=disp,
254
+ )
255
+ all_result = pd.concat([all_result, one_result])
256
+
257
+ all_result = all_result.sort_values(by="score", ascending=False)
258
+ all_result["rank"] = all_result["score"].rank(ascending=False)
259
+
260
+ return all_result
261
+
262
+ # Get node summary - a function
263
+ @staticmethod
264
+ def _GetNodeSummary(
265
+ data,
266
+ experiment_group_column="treatment_group_key",
267
+ y_name="conversion",
268
+ smooth=True,
269
+ ):
270
+ """
271
+ To count the conversions and get the probabilities by treatment groups. This function comes from the uplift
272
+ tree algorithm, that is used for tree node split evaluation.
273
+
274
+ Parameters
275
+ ----------
276
+ data : DataFrame
277
+ The DataFrame that contains all the data (in the current "node").
278
+ experiment_group_column : str
279
+ Treatment indicator column name.
280
+ y_name : str
281
+ Label indicator column name.
282
+ smooth : bool
283
+ Smooth label count by adding 1 in case certain labels do not occur
284
+ naturally with a treatment. Prevents zero divisions.
285
+
286
+ Returns
287
+ -------
288
+ results : dict
289
+ Counts of conversions by treatment groups, of the form:
290
+ {'control': {0: 10, 1: 8}, 'treatment1': {0: 5, 1: 15}}
291
+ nodeSummary: dict
292
+ Probability of conversion and group size by treatment groups, of
293
+ the form:
294
+ {'control': [0.490, 500], 'treatment1': [0.584, 500]}
295
+ """
296
+
297
+ # Note: results and nodeSummary are both dict with treatment_group_key
298
+ # as the key. So we can compute the treatment effect and/or
299
+ # divergence easily.
300
+
301
+ # Counts of conversions by treatment group
302
+ results_series = data.groupby([experiment_group_column, y_name]).size()
303
+
304
+ treatment_group_keys = results_series.index.levels[0].tolist()
305
+ y_name_keys = results_series.index.levels[1].tolist()
306
+
307
+ results = {}
308
+ for ti in treatment_group_keys:
309
+ results.update({ti: {}})
310
+ for ci in y_name_keys:
311
+ if smooth:
312
+ results[ti].update(
313
+ {
314
+ ci: (
315
+ results_series[ti, ci]
316
+ if results_series.index.isin([(ti, ci)]).any()
317
+ else 1
318
+ )
319
+ }
320
+ )
321
+ else:
322
+ results[ti].update({ci: results_series[ti, ci]})
323
+
324
+ # Probability of conversion and group size by treatment group
325
+ nodeSummary = {}
326
+ for treatment_group_key in results:
327
+ n_1 = results[treatment_group_key].get(1, 0)
328
+ n_total = results[treatment_group_key].get(1, 0) + results[
329
+ treatment_group_key
330
+ ].get(0, 0)
331
+ y_mean = 1.0 * n_1 / n_total
332
+ nodeSummary[treatment_group_key] = [y_mean, n_total]
333
+
334
+ return results, nodeSummary
335
+
336
+ # Divergence-related functions, from upliftpy
337
+ @staticmethod
338
+ def _kl_divergence(pk, qk):
339
+ """
340
+ Calculate KL Divergence for binary classification.
341
+
342
+ Args:
343
+ pk (float): Probability of class 1 in treatment group
344
+ qk (float): Probability of class 1 in control group
345
+ """
346
+ if qk < 0.1**6:
347
+ qk = 0.1**6
348
+ elif qk > 1 - 0.1**6:
349
+ qk = 1 - 0.1**6
350
+ S = pk * np.log(pk / qk) + (1 - pk) * np.log((1 - pk) / (1 - qk))
351
+ return S
352
+
353
+ def _evaluate_KL(self, nodeSummary, control_group="control"):
354
+ """
355
+ Calculate the multi-treatment unconditional D (one node)
356
+ with KL Divergence as split Evaluation function.
357
+
358
+ Args:
359
+ nodeSummary (dict): a dictionary containing the statistics for a tree node sample
360
+ control_group (string, optional, default='control'): the name for control group
361
+
362
+ Notes
363
+ -----
364
+ The function works for more than one non-control treatment groups.
365
+ """
366
+ if control_group not in nodeSummary:
367
+ return 0
368
+ pc = nodeSummary[control_group][0]
369
+ d_res = 0
370
+ for treatment_group in nodeSummary:
371
+ if treatment_group != control_group:
372
+ d_res += self._kl_divergence(nodeSummary[treatment_group][0], pc)
373
+ return d_res
374
+
375
+ @staticmethod
376
+ def _evaluate_ED(nodeSummary, control_group="control"):
377
+ """
378
+ Calculate the multi-treatment unconditional D (one node)
379
+ with Euclidean Distance as split Evaluation function.
380
+
381
+ Args:
382
+ nodeSummary (dict): a dictionary containing the statistics for a tree node sample
383
+ control_group (string, optional, default='control'): the name for control group
384
+ """
385
+ if control_group not in nodeSummary:
386
+ return 0
387
+ pc = nodeSummary[control_group][0]
388
+ d_res = 0
389
+ for treatment_group in nodeSummary:
390
+ if treatment_group != control_group:
391
+ d_res += 2 * (nodeSummary[treatment_group][0] - pc) ** 2
392
+ return d_res
393
+
394
+ @staticmethod
395
+ def _evaluate_Chi(nodeSummary, control_group="control"):
396
+ """
397
+ Calculate the multi-treatment unconditional D (one node)
398
+ with Chi-Square as split Evaluation function.
399
+
400
+ Args:
401
+ nodeSummary (dict): a dictionary containing the statistics for a tree node sample
402
+ control_group (string, optional, default='control'): the name for control group
403
+ """
404
+ if control_group not in nodeSummary:
405
+ return 0
406
+ pc = nodeSummary[control_group][0]
407
+ d_res = 0
408
+ for treatment_group in nodeSummary:
409
+ if treatment_group != control_group:
410
+ d_res += (nodeSummary[treatment_group][0] - pc) ** 2 / max(
411
+ 0.1**6, pc
412
+ ) + (nodeSummary[treatment_group][0] - pc) ** 2 / max(0.1**6, 1 - pc)
413
+ return d_res
414
+
415
+ def _filter_D_one_feature(
416
+ self,
417
+ data,
418
+ feature_name,
419
+ y_name,
420
+ n_bins=10,
421
+ method="KL",
422
+ control_group="control",
423
+ experiment_group_column="treatment_group_key",
424
+ null_impute=None,
425
+ ):
426
+ """
427
+ Calculate the chosen divergence measure for one feature.
428
+
429
+ Args:
430
+ data (pd.Dataframe): DataFrame containing outcome, features, and experiment group
431
+ treatment_indicator (string): the column name for binary indicator of treatment (1) or control (0)
432
+ feature_name (string): feature name, as one column in the data DataFrame
433
+ y_name (string): name of the outcome variable
434
+ method (string, optional, default = 'KL'): taking one of the following values {'F', 'LR', 'KL', 'ED', 'Chi'}
435
+ The feature selection method to be used to rank the features.
436
+ 'F' for F-test
437
+ 'LR' for likelihood ratio test
438
+ 'KL', 'ED', 'Chi' for bin-based uplift filter methods, KL divergence, Euclidean distance,
439
+ Chi-Square respectively
440
+ experiment_group_column (string, optional, default = 'treatment_group_key'): the experiment column name in
441
+ the DataFrame, which contains the treatment and control assignment label
442
+ control_group (string, optional, default = 'control'): name for control group, value in the experiment
443
+ group column
444
+ n_bins (int, optional, default = 10): number of bins to be used for bin-based uplift filter methods
445
+ null_impute (str, optional, default=None): impute np.nan present in the data taking on of the following
446
+ strategy values {'mean', 'median', 'most_frequent', None}. If Value is None and null is present then
447
+ exception will be raised
448
+
449
+ Returns:
450
+ D_result : pd.DataFrame
451
+ a data frame containing the feature importance statistics
452
+ """
453
+ # [TODO] Application to categorical features
454
+
455
+ if method == "KL":
456
+ evaluationFunction = self._evaluate_KL
457
+ elif method == "ED":
458
+ evaluationFunction = self._evaluate_ED
459
+ elif method == "Chi":
460
+ evaluationFunction = self._evaluate_Chi
461
+
462
+ totalSize = len(data.index)
463
+
464
+ # impute null if enabled
465
+ if null_impute is not None:
466
+ data[feature_name] = SimpleImputer(
467
+ missing_values=np.nan, strategy=null_impute
468
+ ).fit_transform(data[feature_name].values.reshape(-1, 1))
469
+ elif data[feature_name].isna().any():
470
+ raise Exception(
471
+ "Null value(s) present in column '{}'. Please impute the null value or use null_impute parameter "
472
+ "provided.".format(feature_name)
473
+ )
474
+
475
+ # drop duplicate edges in pq.cut result to avoid issues
476
+ x_bin = pd.qcut(
477
+ data[feature_name].values, n_bins, labels=False, duplicates="drop"
478
+ )
479
+
480
+ d_children = 0
481
+
482
+ for i_bin in range(np.nanmax(x_bin).astype(int) + 1): # range(n_bins):
483
+ nodeSummary = self._GetNodeSummary(
484
+ data=data.loc[x_bin == i_bin],
485
+ experiment_group_column=experiment_group_column,
486
+ y_name=y_name,
487
+ )[1]
488
+ nodeScore = evaluationFunction(nodeSummary, control_group=control_group)
489
+ nodeSize = sum([x[1] for x in list(nodeSummary.values())])
490
+ d_children += nodeScore * nodeSize / totalSize
491
+
492
+ parentNodeSummary = self._GetNodeSummary(
493
+ data=data, experiment_group_column=experiment_group_column, y_name=y_name
494
+ )[1]
495
+ d_parent = evaluationFunction(parentNodeSummary, control_group=control_group)
496
+
497
+ d_res = d_children - d_parent
498
+
499
+ D_result = pd.DataFrame(
500
+ {
501
+ "feature": feature_name,
502
+ "method": method,
503
+ "score": d_res,
504
+ "p_value": None,
505
+ "misc": "number_of_bins: {}".format(
506
+ min(n_bins, np.nanmax(x_bin).astype(int) + 1)
507
+ ), # format(n_bins),
508
+ },
509
+ index=[0],
510
+ ).reset_index(drop=True)
511
+
512
+ return D_result
513
+
514
+ def filter_D(
515
+ self,
516
+ data,
517
+ features,
518
+ y_name,
519
+ n_bins=10,
520
+ method="KL",
521
+ control_group="control",
522
+ experiment_group_column="treatment_group_key",
523
+ null_impute=None,
524
+ ):
525
+ """
526
+ Rank features based on the chosen divergence measure.
527
+
528
+ Args:
529
+ data (pd.Dataframe): DataFrame containing outcome, features, and experiment group
530
+ treatment_indicator (string): the column name for binary indicator of treatment (1) or control (0)
531
+ features (list of string): list of feature names, that are columns in the data DataFrame
532
+ y_name (string): name of the outcome variable
533
+ method (string, optional, default = 'KL'): taking one of the following values {'F', 'LR', 'KL', 'ED', 'Chi'}
534
+ The feature selection method to be used to rank the features.
535
+ 'F' for F-test
536
+ 'LR' for likelihood ratio test
537
+ 'KL', 'ED', 'Chi' for bin-based uplift filter methods, KL divergence, Euclidean distance, Chi-Square
538
+ respectively
539
+ experiment_group_column (string, optional, default = 'treatment_group_key'): the experiment column name in
540
+ the DataFrame, which contains the treatment and control assignment label
541
+ control_group (string, optional, default = 'control'): name for control group, value in the experiment
542
+ group column
543
+ n_bins (int, optional, default = 10): number of bins to be used for bin-based uplift filter methods
544
+ null_impute (str, optional, default=None): impute np.nan present in the data taking on of the followin
545
+ strategy values {'mean', 'median', 'most_frequent', None}. If Value is None and null is present then
546
+ exception will be raised
547
+
548
+ Returns:
549
+ all_result : pd.DataFrame
550
+ a data frame containing the feature importance statistics
551
+ """
552
+
553
+ all_result = pd.DataFrame()
554
+
555
+ for x_name_i in features:
556
+ one_result = self._filter_D_one_feature(
557
+ data=data,
558
+ feature_name=x_name_i,
559
+ y_name=y_name,
560
+ n_bins=n_bins,
561
+ method=method,
562
+ control_group=control_group,
563
+ experiment_group_column=experiment_group_column,
564
+ null_impute=null_impute,
565
+ )
566
+ all_result = pd.concat([all_result, one_result])
567
+
568
+ all_result = all_result.sort_values(by="score", ascending=False)
569
+ all_result["rank"] = all_result["score"].rank(ascending=False)
570
+
571
+ return all_result
572
+
573
+ def get_importance(
574
+ self,
575
+ data,
576
+ features,
577
+ y_name,
578
+ method,
579
+ experiment_group_column="treatment_group_key",
580
+ control_group="control",
581
+ treatment_group="treatment",
582
+ n_bins=5,
583
+ null_impute=None,
584
+ order=1,
585
+ disp=False,
586
+ ):
587
+ """
588
+ Rank features based on the chosen statistic of the interaction.
589
+
590
+ Args:
591
+ data (pd.Dataframe): DataFrame containing outcome, features, and experiment group
592
+ features (list of string): list of feature names, that are columns in the data DataFrame
593
+ y_name (string): name of the outcome variable
594
+ method (string, optional, default = 'KL'): taking one of the following values {'F', 'LR', 'KL', 'ED', 'Chi'}
595
+ The feature selection method to be used to rank the features.
596
+ 'F' for F-test
597
+ 'LR' for likelihood ratio test
598
+ 'KL', 'ED', 'Chi' for bin-based uplift filter methods, KL divergence, Euclidean distance, Chi-Square
599
+ respectively
600
+ experiment_group_column (string): the experiment column name in the DataFrame, which contains the treatment
601
+ and control assignment label
602
+ control_group (string): name for control group, value in the experiment group column
603
+ treatment_group (string): name for treatment group, value in the experiment group column
604
+ n_bins (int, optional): number of bins to be used for bin-based uplift filter methods
605
+ null_impute (str, optional, default=None): impute np.nan present in the data taking on of the following
606
+ strategy values {'mean', 'median', 'most_frequent', None}. If value is None and null is present then
607
+ exception will be raised
608
+ order (int): the order of feature to be evaluated with the treatment effect for F filter and LR filter,
609
+ order takes 3 values: 1,2,3. order = 1 corresponds to linear importance of the feature, order=2
610
+ corresponds to quadratic and linear importance of the feature,
611
+ order= 3 will calculate feature importance up to cubic forms.
612
+ disp (bool): Set to True to print convergence messages for Logistic regression convergence in LR method.
613
+
614
+ Returns:
615
+ all_result : pd.DataFrame
616
+ a data frame with following columns: ['method', 'feature', 'rank', 'score', 'p_value', 'misc']
617
+ """
618
+
619
+ if method == "F":
620
+ data = data[
621
+ data[experiment_group_column].isin([control_group, treatment_group])
622
+ ]
623
+ data["treatment_indicator"] = 0
624
+ data.loc[
625
+ data[experiment_group_column] == treatment_group, "treatment_indicator"
626
+ ] = 1
627
+ all_result = self.filter_F(
628
+ data=data,
629
+ treatment_indicator="treatment_indicator",
630
+ features=features,
631
+ y_name=y_name,
632
+ order=order,
633
+ )
634
+ elif method == "LR":
635
+ data = data[
636
+ data[experiment_group_column].isin([control_group, treatment_group])
637
+ ]
638
+ data["treatment_indicator"] = 0
639
+ data.loc[
640
+ data[experiment_group_column] == treatment_group, "treatment_indicator"
641
+ ] = 1
642
+ all_result = self.filter_LR(
643
+ data=data,
644
+ disp=disp,
645
+ treatment_indicator="treatment_indicator",
646
+ features=features,
647
+ y_name=y_name,
648
+ order=order,
649
+ )
650
+ else:
651
+ all_result = self.filter_D(
652
+ data=data,
653
+ method=method,
654
+ features=features,
655
+ y_name=y_name,
656
+ n_bins=n_bins,
657
+ control_group=control_group,
658
+ experiment_group_column=experiment_group_column,
659
+ null_impute=null_impute,
660
+ )
661
+
662
+ all_result["method"] = method + " filter"
663
+ return all_result[["method", "feature", "rank", "score", "p_value", "misc"]]
causalml/source/causalml/features.py ADDED
@@ -0,0 +1,267 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+ import numpy as np
3
+ import pandas as pd
4
+ from scipy import sparse
5
+ from sklearn import base
6
+
7
+ logger = logging.getLogger("causalml")
8
+
9
+
10
+ NAN_INT = -98765 # A random integer to impute missing values with
11
+
12
+
13
+ class LabelEncoder(base.BaseEstimator):
14
+ """Label Encoder that groups infrequent values into one label.
15
+
16
+ Code from https://github.com/jeongyoonlee/Kaggler/blob/master/kaggler/preprocessing/data.py
17
+
18
+ Attributes:
19
+ min_obs (int): minimum number of observation to assign a label.
20
+ label_encoders (list of dict): label encoders for columns
21
+ label_maxes (list of int): maximum of labels for columns
22
+ """
23
+
24
+ def __init__(self, min_obs=10):
25
+ """Initialize the LabelEncoder class object.
26
+
27
+ Args:
28
+ min_obs (int): minimum number of observation to assign a label.
29
+ """
30
+
31
+ self.min_obs = min_obs
32
+
33
+ def __repr__(self):
34
+ return ("LabelEncoder(min_obs={})").format(self.min_obs)
35
+
36
+ def _get_label_encoder_and_max(self, x):
37
+ """Return a mapping from values and its maximum of a column to integer labels.
38
+
39
+ Args:
40
+ x (pandas.Series): a categorical column to encode.
41
+
42
+ Returns:
43
+ label_encoder (dict): mapping from values of features to integers
44
+ max_label (int): maximum label
45
+ """
46
+
47
+ # NaN cannot be used as a key for dict. So replace it with a random integer.
48
+ label_count = x.fillna(NAN_INT).value_counts()
49
+ n_uniq = label_count.shape[0]
50
+
51
+ label_count = label_count[label_count >= self.min_obs]
52
+ n_uniq_new = label_count.shape[0]
53
+
54
+ # If every label appears more than min_obs, new label starts from 0.
55
+ # Otherwise, new label starts from 1 and 0 is used for all old labels
56
+ # that appear less than min_obs.
57
+ offset = 0 if n_uniq == n_uniq_new else 1
58
+
59
+ label_encoder = pd.Series(
60
+ np.arange(n_uniq_new) + offset, index=label_count.index
61
+ )
62
+ max_label = label_encoder.max()
63
+ label_encoder = label_encoder.to_dict()
64
+
65
+ return label_encoder, max_label
66
+
67
+ def _transform_col(self, x, i):
68
+ """Encode one categorical column into labels.
69
+
70
+ Args:
71
+ x (pandas.Series): a categorical column to encode
72
+ i (int): column index
73
+
74
+ Returns:
75
+ x (pandas.Series): a column with labels.
76
+ """
77
+ return x.fillna(NAN_INT).map(self.label_encoders[i]).fillna(0)
78
+
79
+ def fit(self, X, y=None):
80
+ self.label_encoders = [None] * X.shape[1]
81
+ self.label_maxes = [None] * X.shape[1]
82
+
83
+ for i, col in enumerate(X.columns):
84
+ (
85
+ self.label_encoders[i],
86
+ self.label_maxes[i],
87
+ ) = self._get_label_encoder_and_max(X[col])
88
+
89
+ return self
90
+
91
+ def transform(self, X):
92
+ """Encode categorical columns into label encoded columns
93
+
94
+ Args:
95
+ X (pandas.DataFrame): categorical columns to encode
96
+
97
+ Returns:
98
+ X (pandas.DataFrame): label encoded columns
99
+ """
100
+ X = X.copy()
101
+ for i, col in enumerate(X.columns):
102
+ X[col] = self._transform_col(X[col], i).astype(float)
103
+
104
+ return X
105
+
106
+ def fit_transform(self, X, y=None):
107
+ """Encode categorical columns into label encoded columns
108
+
109
+ Args:
110
+ X (pandas.DataFrame): categorical columns to encode
111
+
112
+ Returns:
113
+ X (pandas.DataFrame): label encoded columns
114
+ """
115
+ X = X.copy()
116
+ self.label_encoders = [None] * X.shape[1]
117
+ self.label_maxes = [None] * X.shape[1]
118
+
119
+ for i, col in enumerate(X.columns):
120
+ (
121
+ self.label_encoders[i],
122
+ self.label_maxes[i],
123
+ ) = self._get_label_encoder_and_max(X[col])
124
+
125
+ X[col] = (
126
+ X[col]
127
+ .fillna(NAN_INT)
128
+ .map(self.label_encoders[i])
129
+ .fillna(0)
130
+ .astype(float)
131
+ )
132
+
133
+ return X
134
+
135
+
136
+ class OneHotEncoder(base.BaseEstimator):
137
+ """One-Hot-Encoder that groups infrequent values into one dummy variable.
138
+
139
+ Code from https://github.com/jeongyoonlee/Kaggler/blob/master/kaggler/preprocessing/data.py
140
+
141
+ Attributes:
142
+ min_obs (int): minimum number of observation to create a dummy variable
143
+ label_encoders (list of (dict, int)): label encoders and their maximums
144
+ for columns
145
+ """
146
+
147
+ def __init__(self, min_obs=10):
148
+ """Initialize the OneHotEncoder class object.
149
+
150
+ Args:
151
+ min_obs (int): minimum number of observation to create a dummy variable
152
+ """
153
+
154
+ self.min_obs = min_obs
155
+ self.label_encoder = LabelEncoder(min_obs)
156
+
157
+ def __repr__(self):
158
+ return ("OneHotEncoder(min_obs={})").format(self.min_obs)
159
+
160
+ def _transform_col(self, x, i):
161
+ """Encode one categorical column into sparse matrix with one-hot-encoding.
162
+
163
+ Args:
164
+ x (pandas.Series): a categorical column to encode
165
+ i (int): column index
166
+
167
+ Returns:
168
+ X (scipy.sparse.coo_matrix): sparse matrix encoding a categorical
169
+ variable into dummy variables
170
+ """
171
+
172
+ labels = self.label_encoder._transform_col(x, i)
173
+ label_max = self.label_encoder.label_maxes[i]
174
+
175
+ # build row and column index for non-zero values of a sparse matrix
176
+ index = np.array(range(len(labels)))
177
+ i = index[labels > 0]
178
+ j = labels[labels > 0] - 1 # column index starts from 0
179
+
180
+ if len(i) > 0:
181
+ return sparse.coo_matrix(
182
+ (np.ones_like(i), (i, j)), shape=(x.shape[0], label_max)
183
+ )
184
+ else:
185
+ # if there is no non-zero value, return no matrix
186
+ return None
187
+
188
+ def fit(self, X, y=None):
189
+ self.label_encoder.fit(X)
190
+
191
+ return self
192
+
193
+ def transform(self, X):
194
+ """Encode categorical columns into sparse matrix with one-hot-encoding.
195
+
196
+ Args:
197
+ X (pandas.DataFrame): categorical columns to encode
198
+
199
+ Returns:
200
+ X_new (scipy.sparse.coo_matrix): sparse matrix encoding categorical
201
+ variables into dummy variables
202
+ """
203
+
204
+ X_new = None
205
+ for i, col in enumerate(X.columns):
206
+ X_col = self._transform_col(X[col], i)
207
+ if X_col is not None:
208
+ if X_new is None:
209
+ X_new = X_col
210
+ else:
211
+ X_new = sparse.hstack((X_new, X_col))
212
+
213
+ logger.debug(
214
+ "{} --> {} features".format(col, self.label_encoder.label_maxes[i])
215
+ )
216
+
217
+ assert (
218
+ X_new is not None
219
+ ), "no column was transformed, please check your dataframe input"
220
+ return X_new
221
+
222
+ def fit_transform(self, X, y=None):
223
+ """Encode categorical columns into sparse matrix with one-hot-encoding.
224
+
225
+ Args:
226
+ X (pandas.DataFrame): categorical columns to encode
227
+
228
+ Returns:
229
+ sparse matrix encoding categorical variables into dummy variables
230
+ """
231
+
232
+ self.label_encoder.fit(X)
233
+
234
+ return self.transform(X)
235
+
236
+
237
+ def load_data(data, features, transformations={}):
238
+ """Load data and set the feature matrix and label vector.
239
+
240
+ Args:
241
+ data (pandas.DataFrame): total input data
242
+ features (list of str): column names to be used in the inference model
243
+ transformation (dict of (str, func)): transformations to be applied to features
244
+
245
+ Returns:
246
+ X (numpy.matrix): a feature matrix
247
+ """
248
+
249
+ df = data[features].copy()
250
+
251
+ bool_cols = [col for col in df.columns if df[col].dtype == bool]
252
+ df.loc[:, bool_cols] = df[bool_cols].astype(int)
253
+
254
+ for col, transformation in transformations.items():
255
+ logger.info("Applying {} to {}".format(transformation.__name__, col))
256
+ df[col] = df[col].apply(transformation)
257
+
258
+ cat_cols = [col for col in features if not pd.api.types.is_numeric_dtype(df[col])]
259
+ num_cols = [col for col in features if col not in cat_cols]
260
+
261
+ logger.info("Applying one-hot-encoding to {}".format(cat_cols))
262
+ ohe = OneHotEncoder(min_obs=df.shape[0] * 0.001)
263
+ X_cat = ohe.fit_transform(df[cat_cols]).todense()
264
+
265
+ X = np.hstack([df[num_cols].values, X_cat])
266
+
267
+ return X
causalml/source/causalml/inference/__init__.py ADDED
File without changes
causalml/source/causalml/inference/iv/__init__.py ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ from .iv_regression import IVRegressor
2
+ from .drivlearner import BaseDRIVLearner, BaseDRIVRegressor, XGBDRIVRegressor
causalml/source/causalml/inference/iv/drivlearner.py ADDED
@@ -0,0 +1,881 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+ from copy import deepcopy
3
+
4
+ import numpy as np
5
+ import pandas as pd
6
+ from causalml.inference.meta.explainer import Explainer
7
+ from causalml.inference.meta.utils import (
8
+ check_treatment_vector,
9
+ check_p_conditions,
10
+ convert_pd_to_np,
11
+ )
12
+ from causalml.metrics import regression_metrics
13
+ from causalml.propensity import compute_propensity_score
14
+ from scipy.stats import norm
15
+ from sklearn.model_selection import KFold
16
+ from tqdm import tqdm
17
+ from xgboost import XGBRegressor
18
+
19
+ logger = logging.getLogger("causalml")
20
+
21
+
22
+ class BaseDRIVLearner:
23
+ """A parent class for DRIV-learner regressor classes.
24
+
25
+ A DRIV-learner estimates endogenous treatment effects for compliers with machine learning models.
26
+
27
+ Details of DR-learner are available at `Kennedy (2020) <https://arxiv.org/abs/2004.14497>`_.
28
+ The DR moment condition for LATE comes from
29
+ `Chernozhukov et al (2018) <https://academic.oup.com/ectj/article/21/1/C1/5056401>`_.
30
+ """
31
+
32
+ def __init__(
33
+ self,
34
+ learner=None,
35
+ control_outcome_learner=None,
36
+ treatment_outcome_learner=None,
37
+ treatment_effect_learner=None,
38
+ ate_alpha=0.05,
39
+ control_name=0,
40
+ ):
41
+ """Initialize a DR-learner.
42
+
43
+ Args:
44
+ learner (optional): a model to estimate outcomes and treatment effects in both the control and treatment
45
+ groups
46
+ control_outcome_learner (optional): a model to estimate outcomes in the control group
47
+ treatment_outcome_learner (optional): a model to estimate outcomes in the treatment group
48
+ treatment_effect_learner (optional): a model to estimate treatment effects in the treatment group. It needs
49
+ to take `sample_weight` as an input argument in `fit()`.
50
+ ate_alpha (float, optional): the confidence level alpha of the ATE estimate
51
+ control_name (str or int, optional): name of control group
52
+ """
53
+ assert (learner is not None) or (
54
+ (control_outcome_learner is not None)
55
+ and (treatment_outcome_learner is not None)
56
+ and (treatment_effect_learner is not None)
57
+ )
58
+
59
+ if control_outcome_learner is None:
60
+ self.model_mu_c = deepcopy(learner)
61
+ else:
62
+ self.model_mu_c = control_outcome_learner
63
+
64
+ if treatment_outcome_learner is None:
65
+ self.model_mu_t = deepcopy(learner)
66
+ else:
67
+ self.model_mu_t = treatment_outcome_learner
68
+
69
+ if treatment_effect_learner is None:
70
+ self.model_tau = deepcopy(learner)
71
+ else:
72
+ self.model_tau = treatment_effect_learner
73
+
74
+ self.ate_alpha = ate_alpha
75
+ self.control_name = control_name
76
+
77
+ self.propensity_1 = None
78
+ self.propensity_0 = None
79
+ self.propensity_assign = None
80
+
81
+ def __repr__(self):
82
+ return (
83
+ "{}(control_outcome_learner={},\n"
84
+ "\ttreatment_outcome_learner={},\n"
85
+ "\ttreatment_effect_learner={})".format(
86
+ self.__class__.__name__,
87
+ self.model_mu_c.__repr__(),
88
+ self.model_mu_t.__repr__(),
89
+ self.model_tau.__repr__(),
90
+ )
91
+ )
92
+
93
+ def fit(
94
+ self, X, assignment, treatment, y, p=None, pZ=None, seed=None, calibrate=True
95
+ ):
96
+ """Fit the inference model.
97
+
98
+ Args:
99
+ X (np.matrix or np.array or pd.Dataframe): a feature matrix
100
+ assignment (np.array or pd.Series): a (0,1)-valued assignment vector. The assignment is the
101
+ instrumental variable that does not depend on unknown confounders. The assignment status
102
+ influences treatment in a monotonic way, i.e. one can only be more likely to take the
103
+ treatment if assigned.
104
+ treatment (np.array or pd.Series): a treatment vector
105
+ y (np.array or pd.Series): an outcome vector
106
+ p (2-tuple of np.ndarray or pd.Series or dict, optional): The first (second) element corresponds to
107
+ unassigned (assigned) units. Each is an array of propensity scores of float (0,1) in the
108
+ single-treatment case; or, a dictionary of treatment groups that map to propensity vectors of float
109
+ (0,1). If None will run ElasticNetPropensityModel() to generate the propensity scores.
110
+ pZ (np.array or pd.Series, optional): an array of assignment probability of float (0,1); if None
111
+ will run ElasticNetPropensityModel() to generate the assignment probability score.
112
+ seed (int): random seed for cross-fitting
113
+ """
114
+ X, treatment, assignment, y = convert_pd_to_np(X, treatment, assignment, y)
115
+ check_treatment_vector(treatment, self.control_name)
116
+ self.t_groups = np.unique(treatment[treatment != self.control_name])
117
+ self.t_groups.sort()
118
+ self._classes = {group: i for i, group in enumerate(self.t_groups)}
119
+
120
+ # The estimator splits the data into 3 partitions for cross-fit on the propensity score estimation,
121
+ # the outcome regression, and the treatment regression on the doubly robust estimates. The use of
122
+ # the partitions is rotated so we do not lose on the sample size. We do not cross-fit the assignment
123
+ # score estimation as the assignment process is usually simple.
124
+ cv = KFold(n_splits=3, shuffle=True, random_state=seed)
125
+ split_indices = [index for _, index in cv.split(y)]
126
+
127
+ self.models_mu_c = {
128
+ group: [
129
+ deepcopy(self.model_mu_c),
130
+ deepcopy(self.model_mu_c),
131
+ deepcopy(self.model_mu_c),
132
+ ]
133
+ for group in self.t_groups
134
+ }
135
+ self.models_mu_t = {
136
+ group: [
137
+ deepcopy(self.model_mu_t),
138
+ deepcopy(self.model_mu_t),
139
+ deepcopy(self.model_mu_t),
140
+ ]
141
+ for group in self.t_groups
142
+ }
143
+ self.models_tau = {
144
+ group: [
145
+ deepcopy(self.model_tau),
146
+ deepcopy(self.model_tau),
147
+ deepcopy(self.model_tau),
148
+ ]
149
+ for group in self.t_groups
150
+ }
151
+
152
+ if p is None:
153
+ self.propensity_1 = {
154
+ group: np.zeros(y.shape[0]) for group in self.t_groups
155
+ } # propensity scores for those assigned
156
+ self.propensity_0 = {
157
+ group: np.zeros(y.shape[0]) for group in self.t_groups
158
+ } # propensity scores for those not assigned
159
+ if pZ is None:
160
+ self.propensity_assign, _ = compute_propensity_score(
161
+ X=X,
162
+ treatment=assignment,
163
+ X_pred=X,
164
+ treatment_pred=assignment,
165
+ calibrate_p=calibrate,
166
+ )
167
+ else:
168
+ self.propensity_assign = pZ
169
+
170
+ for ifold in range(3):
171
+ treatment_idx = split_indices[ifold]
172
+ outcome_idx = split_indices[(ifold + 1) % 3]
173
+ tau_idx = split_indices[(ifold + 2) % 3]
174
+
175
+ treatment_treat, treatment_out, treatment_tau = (
176
+ treatment[treatment_idx],
177
+ treatment[outcome_idx],
178
+ treatment[tau_idx],
179
+ )
180
+ assignment_treat, assignment_out, assignment_tau = (
181
+ assignment[treatment_idx],
182
+ assignment[outcome_idx],
183
+ assignment[tau_idx],
184
+ )
185
+ y_out, y_tau = y[outcome_idx], y[tau_idx]
186
+ X_treat, X_out, X_tau = X[treatment_idx], X[outcome_idx], X[tau_idx]
187
+ pZ_tau = self.propensity_assign[tau_idx]
188
+
189
+ if p is None:
190
+ logger.info("Generating propensity score")
191
+ cur_p_1 = dict()
192
+ cur_p_0 = dict()
193
+
194
+ for group in self.t_groups:
195
+ mask = (treatment_treat == group) | (
196
+ treatment_treat == self.control_name
197
+ )
198
+ mask_1, mask_0 = (
199
+ mask & (assignment_treat == 1),
200
+ mask & (assignment_treat == 0),
201
+ )
202
+ cur_p_1[group], _ = compute_propensity_score(
203
+ X=X_treat[mask_1],
204
+ treatment=(treatment_treat[mask_1] == group).astype(int),
205
+ X_pred=X_tau,
206
+ treatment_pred=(treatment_tau == group).astype(int),
207
+ )
208
+ if (treatment_treat[mask_0] == group).sum() == 0:
209
+ cur_p_0[group] = np.zeros(X_tau.shape[0])
210
+ else:
211
+ cur_p_0[group], _ = compute_propensity_score(
212
+ X=X_treat[mask_0],
213
+ treatment=(treatment_treat[mask_0] == group).astype(int),
214
+ X_pred=X_tau,
215
+ treatment_pred=(treatment_tau == group).astype(int),
216
+ )
217
+ self.propensity_1[group][tau_idx] = cur_p_1[group]
218
+ self.propensity_0[group][tau_idx] = cur_p_0[group]
219
+ else:
220
+ cur_p_1 = dict()
221
+ cur_p_0 = dict()
222
+ if isinstance(p[0], (np.ndarray, pd.Series)):
223
+ cur_p_0 = {self.t_groups[0]: convert_pd_to_np(p[0][tau_idx])}
224
+ else:
225
+ cur_p_0 = {g: prop[tau_idx] for g, prop in p[0].items()}
226
+ check_p_conditions(cur_p_0, self.t_groups)
227
+
228
+ if isinstance(p[1], (np.ndarray, pd.Series)):
229
+ cur_p_1 = {self.t_groups[0]: convert_pd_to_np(p[1][tau_idx])}
230
+ else:
231
+ cur_p_1 = {g: prop[tau_idx] for g, prop in p[1].items()}
232
+ check_p_conditions(cur_p_1, self.t_groups)
233
+
234
+ logger.info("Generate outcome regressions")
235
+ for group in self.t_groups:
236
+ mask = (treatment_out == group) | (treatment_out == self.control_name)
237
+ mask_1, mask_0 = (
238
+ mask & (assignment_out == 1),
239
+ mask & (assignment_out == 0),
240
+ )
241
+ self.models_mu_c[group][ifold].fit(X_out[mask_0], y_out[mask_0])
242
+ self.models_mu_t[group][ifold].fit(X_out[mask_1], y_out[mask_1])
243
+
244
+ logger.info("Fit pseudo outcomes from the DR formula")
245
+
246
+ for group in self.t_groups:
247
+ mask = (treatment_tau == group) | (treatment_tau == self.control_name)
248
+ treatment_filt = treatment_tau[mask]
249
+ X_filt = X_tau[mask]
250
+ y_filt = y_tau[mask]
251
+ w_filt = (treatment_filt == group).astype(int)
252
+ p_1_filt = cur_p_1[group][mask]
253
+ p_0_filt = cur_p_0[group][mask]
254
+ z_filt = assignment_tau[mask]
255
+ pZ_filt = pZ_tau[mask]
256
+ mu_t = self.models_mu_t[group][ifold].predict(X_filt)
257
+ mu_c = self.models_mu_c[group][ifold].predict(X_filt)
258
+ dr = (
259
+ z_filt * (y_filt - mu_t) / pZ_filt
260
+ - (1 - z_filt) * (y_filt - mu_c) / (1 - pZ_filt)
261
+ + mu_t
262
+ - mu_c
263
+ )
264
+ weight = (
265
+ z_filt * (w_filt - p_1_filt) / pZ_filt
266
+ - (1 - z_filt) * (w_filt - p_0_filt) / (1 - pZ_filt)
267
+ + p_1_filt
268
+ - p_0_filt
269
+ )
270
+ dr /= weight
271
+ self.models_tau[group][ifold].fit(X_filt, dr, sample_weight=weight**2)
272
+
273
+ def predict(self, X, treatment=None, y=None, return_components=False, verbose=True):
274
+ """Predict treatment effects.
275
+
276
+ Args:
277
+ X (np.matrix or np.array or pd.Dataframe): a feature matrix
278
+ treatment (np.array or pd.Series, optional): a treatment vector
279
+ y (np.array or pd.Series, optional): an outcome vector
280
+ verbose (bool, optional): whether to output progress logs
281
+ Returns:
282
+ (numpy.ndarray): Predictions of treatment effects for compliers, i.e. those individuals
283
+ who take the treatment only if they are assigned.
284
+ """
285
+ X, treatment, y = convert_pd_to_np(X, treatment, y)
286
+
287
+ te = np.zeros((X.shape[0], self.t_groups.shape[0]))
288
+ yhat_cs = {}
289
+ yhat_ts = {}
290
+
291
+ for i, group in enumerate(self.t_groups):
292
+ models_tau = self.models_tau[group]
293
+ _te = np.r_[[model.predict(X) for model in models_tau]].mean(axis=0)
294
+ te[:, i] = np.ravel(_te)
295
+ yhat_cs[group] = np.r_[
296
+ [model.predict(X) for model in self.models_mu_c[group]]
297
+ ].mean(axis=0)
298
+ yhat_ts[group] = np.r_[
299
+ [model.predict(X) for model in self.models_mu_t[group]]
300
+ ].mean(axis=0)
301
+
302
+ if (y is not None) and (treatment is not None) and verbose:
303
+ mask = (treatment == group) | (treatment == self.control_name)
304
+ treatment_filt = treatment[mask]
305
+ y_filt = y[mask]
306
+ w = (treatment_filt == group).astype(int)
307
+
308
+ yhat = np.zeros_like(y_filt, dtype=float)
309
+ yhat[w == 0] = yhat_cs[group][mask][w == 0]
310
+ yhat[w == 1] = yhat_ts[group][mask][w == 1]
311
+
312
+ logger.info("Error metrics for group {}".format(group))
313
+ regression_metrics(y_filt, yhat, w)
314
+
315
+ if not return_components:
316
+ return te
317
+ else:
318
+ return te, yhat_cs, yhat_ts
319
+
320
+ def fit_predict(
321
+ self,
322
+ X,
323
+ assignment,
324
+ treatment,
325
+ y,
326
+ p=None,
327
+ pZ=None,
328
+ return_ci=False,
329
+ n_bootstraps=1000,
330
+ bootstrap_size=10000,
331
+ return_components=False,
332
+ verbose=True,
333
+ seed=None,
334
+ calibrate=True,
335
+ ):
336
+ """Fit the treatment effect and outcome models of the R learner and predict treatment effects.
337
+
338
+ Args:
339
+ X (np.matrix or np.array or pd.Dataframe): a feature matrix
340
+ assignment (np.array or pd.Series): a (0,1)-valued assignment vector. The assignment is the
341
+ instrumental variable that does not depend on unknown confounders. The assignment status
342
+ influences treatment in a monotonic way, i.e. one can only be more likely to take the
343
+ treatment if assigned.
344
+ treatment (np.array or pd.Series): a treatment vector
345
+ y (np.array or pd.Series): an outcome vector
346
+ p (2-tuple of np.ndarray or pd.Series or dict, optional): The first (second) element corresponds to
347
+ unassigned (assigned) units. Each is an array of propensity scores of float (0,1) in the
348
+ single-treatment case; or, a dictionary of treatment groups that map to propensity vectors of float
349
+ (0,1). If None will run ElasticNetPropensityModel() to generate the propensity scores.
350
+ pZ (np.array or pd.Series, optional): an array of assignment probability of float (0,1); if None
351
+ will run ElasticNetPropensityModel() to generate the assignment probability score.
352
+ return_ci (bool): whether to return confidence intervals
353
+ n_bootstraps (int): number of bootstrap iterations
354
+ bootstrap_size (int): number of samples per bootstrap
355
+ return_components (bool, optional): whether to return outcome for treatment and control seperately
356
+ verbose (str): whether to output progress logs
357
+ seed (int): random seed for cross-fitting
358
+ Returns:
359
+ (numpy.ndarray): Predictions of treatment effects for compliers, , i.e. those individuals
360
+ who take the treatment only if they are assigned. Output dim: [n_samples, n_treatment]
361
+ If return_ci, returns CATE [n_samples, n_treatment], LB [n_samples, n_treatment],
362
+ UB [n_samples, n_treatment]
363
+ """
364
+ X, assignment, treatment, y = convert_pd_to_np(X, assignment, treatment, y)
365
+ self.fit(X, assignment, treatment, y, p, seed, calibrate)
366
+
367
+ if p is None:
368
+ p = (self.propensity_0, self.propensity_1)
369
+ else:
370
+ check_p_conditions(p[0], self.t_groups)
371
+ check_p_conditions(p[1], self.t_groups)
372
+
373
+ if isinstance(p[0], (np.ndarray, pd.Series)):
374
+ treatment_name = self.t_groups[0]
375
+ p = (
376
+ {treatment_name: convert_pd_to_np(p[0])},
377
+ {treatment_name: convert_pd_to_np(p[1])},
378
+ )
379
+ elif isinstance(p[0], dict):
380
+ p = (
381
+ {
382
+ treatment_name: convert_pd_to_np(_p)
383
+ for treatment_name, _p in p[0].items()
384
+ },
385
+ {
386
+ treatment_name: convert_pd_to_np(_p)
387
+ for treatment_name, _p in p[1].items()
388
+ },
389
+ )
390
+
391
+ if pZ is None:
392
+ pZ = self.propensity_assign
393
+
394
+ te = self.predict(
395
+ X, treatment=treatment, y=y, return_components=return_components
396
+ )
397
+
398
+ if not return_ci:
399
+ return te
400
+ else:
401
+ t_groups_global = self.t_groups
402
+ _classes_global = self._classes
403
+ models_mu_c_global = deepcopy(self.models_mu_c)
404
+ models_mu_t_global = deepcopy(self.models_mu_t)
405
+ models_tau_global = deepcopy(self.models_tau)
406
+ te_bootstraps = np.zeros(
407
+ shape=(X.shape[0], self.t_groups.shape[0], n_bootstraps)
408
+ )
409
+
410
+ logger.info("Bootstrap Confidence Intervals")
411
+ for i in tqdm(range(n_bootstraps)):
412
+ te_b = self.bootstrap(
413
+ X, assignment, treatment, y, p, pZ, size=bootstrap_size, seed=seed
414
+ )
415
+ te_bootstraps[:, :, i] = te_b
416
+
417
+ te_lower = np.percentile(te_bootstraps, (self.ate_alpha / 2) * 100, axis=2)
418
+ te_upper = np.percentile(
419
+ te_bootstraps, (1 - self.ate_alpha / 2) * 100, axis=2
420
+ )
421
+
422
+ # set member variables back to global (currently last bootstrapped outcome)
423
+ self.t_groups = t_groups_global
424
+ self._classes = _classes_global
425
+ self.models_mu_c = deepcopy(models_mu_c_global)
426
+ self.models_mu_t = deepcopy(models_mu_t_global)
427
+ self.models_tau = deepcopy(models_tau_global)
428
+
429
+ return (te, te_lower, te_upper)
430
+
431
+ def estimate_ate(
432
+ self,
433
+ X,
434
+ assignment,
435
+ treatment,
436
+ y,
437
+ p=None,
438
+ pZ=None,
439
+ bootstrap_ci=False,
440
+ n_bootstraps=1000,
441
+ bootstrap_size=10000,
442
+ seed=None,
443
+ calibrate=True,
444
+ ):
445
+ """Estimate the Average Treatment Effect (ATE) for compliers.
446
+
447
+ Args:
448
+ X (np.matrix or np.array or pd.Dataframe): a feature matrix
449
+ assignment (np.array or pd.Series): an assignment vector. The assignment is the
450
+ instrumental variable that does not depend on unknown confounders. The assignment status
451
+ influences treatment in a monotonic way, i.e. one can only be more likely to take the
452
+ treatment if assigned.
453
+ treatment (np.array or pd.Series): a treatment vector
454
+ y (np.array or pd.Series): an outcome vector
455
+ p (2-tuple of np.ndarray or pd.Series or dict, optional): The first (second) element corresponds to
456
+ unassigned (assigned) units. Each is an array of propensity scores of float (0,1) in the
457
+ single-treatment case; or, a dictionary of treatment groups that map to propensity vectors of float
458
+ (0,1). If None will run ElasticNetPropensityModel() to generate the propensity scores.
459
+ pZ (np.array or pd.Series, optional): an array of assignment probability of float (0,1); if None
460
+ will run ElasticNetPropensityModel() to generate the assignment probability score.
461
+ bootstrap_ci (bool): whether run bootstrap for confidence intervals
462
+ n_bootstraps (int): number of bootstrap iterations
463
+ bootstrap_size (int): number of samples per bootstrap
464
+ seed (int): random seed for cross-fitting
465
+ Returns:
466
+ The mean and confidence interval (LB, UB) of the ATE estimate.
467
+ """
468
+ te, yhat_cs, yhat_ts = self.fit_predict(
469
+ X,
470
+ assignment,
471
+ treatment,
472
+ y,
473
+ p,
474
+ return_components=True,
475
+ seed=seed,
476
+ calibrate=calibrate,
477
+ )
478
+ X, assignment, treatment, y = convert_pd_to_np(X, assignment, treatment, y)
479
+
480
+ if p is None:
481
+ p = (self.propensity_0, self.propensity_1)
482
+ else:
483
+ check_p_conditions(p[0], self.t_groups)
484
+ check_p_conditions(p[1], self.t_groups)
485
+
486
+ if isinstance(p[0], (np.ndarray, pd.Series)):
487
+ treatment_name = self.t_groups[0]
488
+ p = (
489
+ {treatment_name: convert_pd_to_np(p[0])},
490
+ {treatment_name: convert_pd_to_np(p[1])},
491
+ )
492
+ elif isinstance(p[0], dict):
493
+ p = (
494
+ {
495
+ treatment_name: convert_pd_to_np(_p)
496
+ for treatment_name, _p in p[0].items()
497
+ },
498
+ {
499
+ treatment_name: convert_pd_to_np(_p)
500
+ for treatment_name, _p in p[1].items()
501
+ },
502
+ )
503
+
504
+ ate = np.zeros(self.t_groups.shape[0])
505
+ ate_lb = np.zeros(self.t_groups.shape[0])
506
+ ate_ub = np.zeros(self.t_groups.shape[0])
507
+
508
+ for i, group in enumerate(self.t_groups):
509
+ _ate = te[:, i].mean()
510
+
511
+ mask = (treatment == group) | (treatment == self.control_name)
512
+ mask_1, mask_0 = mask & (assignment == 1), mask & (assignment == 0)
513
+ Gamma = (treatment[mask_1] == group).mean() - (
514
+ treatment[mask_0] == group
515
+ ).mean()
516
+
517
+ y_filt_1, y_filt_0 = y[mask_1], y[mask_0]
518
+ yhat_0 = yhat_cs[group][mask_0]
519
+ yhat_1 = yhat_ts[group][mask_1]
520
+ treatment_filt_1, treatment_filt_0 = treatment[mask_1], treatment[mask_0]
521
+ prob_treatment_1, prob_treatment_0 = (
522
+ p[1][group][mask_1],
523
+ p[0][group][mask_0],
524
+ )
525
+ w = (assignment[mask]).mean()
526
+
527
+ part_1 = (
528
+ (y_filt_1 - yhat_1).var()
529
+ + _ate**2 * (treatment_filt_1 - prob_treatment_1).var()
530
+ - 2
531
+ * _ate
532
+ * (y_filt_1 * treatment_filt_1 - yhat_1 * prob_treatment_1).mean()
533
+ )
534
+ part_0 = (
535
+ (y_filt_0 - yhat_0).var()
536
+ + _ate**2 * (treatment_filt_0 - prob_treatment_0).var()
537
+ - 2
538
+ * _ate
539
+ * (y_filt_0 * treatment_filt_0 - yhat_0 * prob_treatment_0).mean()
540
+ )
541
+ part_2 = np.mean(
542
+ (
543
+ yhat_ts[group][mask]
544
+ - yhat_cs[group][mask]
545
+ - _ate * (p[1][group][mask] - p[0][group][mask])
546
+ )
547
+ ** 2
548
+ )
549
+
550
+ # SE formula is based on the lower bound formula (9) from Frölich, Markus. 2006.
551
+ # "Nonparametric IV estimation of local average treatment effects wth covariates."
552
+ # Journal of Econometrics.
553
+ se = np.sqrt((part_1 / w + part_0 / (1 - w)) + part_2) / Gamma
554
+
555
+ _ate_lb = _ate - se * norm.ppf(1 - self.ate_alpha / 2)
556
+ _ate_ub = _ate + se * norm.ppf(1 - self.ate_alpha / 2)
557
+
558
+ ate[i] = _ate
559
+ ate_lb[i] = _ate_lb
560
+ ate_ub[i] = _ate_ub
561
+
562
+ if not bootstrap_ci:
563
+ return ate, ate_lb, ate_ub
564
+ else:
565
+ t_groups_global = self.t_groups
566
+ _classes_global = self._classes
567
+ models_mu_c_global = deepcopy(self.models_mu_c)
568
+ models_mu_t_global = deepcopy(self.models_mu_t)
569
+ models_tau_global = deepcopy(self.models_tau)
570
+
571
+ logger.info("Bootstrap Confidence Intervals for ATE")
572
+ ate_bootstraps = np.zeros(shape=(self.t_groups.shape[0], n_bootstraps))
573
+
574
+ for n in tqdm(range(n_bootstraps)):
575
+ cate_b = self.bootstrap(
576
+ X, assignment, treatment, y, p, pZ, size=bootstrap_size, seed=seed
577
+ )
578
+ ate_bootstraps[:, n] = cate_b.mean()
579
+
580
+ ate_lower = np.percentile(
581
+ ate_bootstraps, (self.ate_alpha / 2) * 100, axis=1
582
+ )
583
+ ate_upper = np.percentile(
584
+ ate_bootstraps, (1 - self.ate_alpha / 2) * 100, axis=1
585
+ )
586
+
587
+ # set member variables back to global (currently last bootstrapped outcome)
588
+ self.t_groups = t_groups_global
589
+ self._classes = _classes_global
590
+ self.models_mu_c = deepcopy(models_mu_c_global)
591
+ self.models_mu_t = deepcopy(models_mu_t_global)
592
+ self.models_tau = deepcopy(models_tau_global)
593
+ return ate, ate_lower, ate_upper
594
+
595
+ def bootstrap(self, X, assignment, treatment, y, p, pZ, size=10000, seed=None):
596
+ """Runs a single bootstrap. Fits on bootstrapped sample, then predicts on whole population."""
597
+ idxs = np.random.choice(np.arange(0, X.shape[0]), size=size)
598
+ X_b = X[idxs]
599
+
600
+ if isinstance(p[0], (np.ndarray, pd.Series)):
601
+ p0_b = {self.t_groups[0]: convert_pd_to_np(p[0][idxs])}
602
+ else:
603
+ p0_b = {g: prop[idxs] for g, prop in p[0].items()}
604
+ if isinstance(p[1], (np.ndarray, pd.Series)):
605
+ p1_b = {self.t_groups[0]: convert_pd_to_np(p[1][idxs])}
606
+ else:
607
+ p1_b = {g: prop[idxs] for g, prop in p[1].items()}
608
+
609
+ pZ_b = pZ[idxs]
610
+ assignment_b = assignment[idxs]
611
+ treatment_b = treatment[idxs]
612
+ y_b = y[idxs]
613
+ self.fit(
614
+ X=X_b,
615
+ assignment=assignment_b,
616
+ treatment=treatment_b,
617
+ y=y_b,
618
+ p=(p0_b, p1_b),
619
+ pZ=pZ_b,
620
+ seed=seed,
621
+ )
622
+ te_b = self.predict(X=X)
623
+ return te_b
624
+
625
+ def get_importance(
626
+ self,
627
+ X=None,
628
+ tau=None,
629
+ model_tau_feature=None,
630
+ features=None,
631
+ method="auto",
632
+ normalize=True,
633
+ test_size=0.3,
634
+ random_state=None,
635
+ ):
636
+ """
637
+ Builds a model (using X to predict estimated/actual tau), and then calculates feature importances
638
+ based on a specified method.
639
+
640
+ Currently supported methods are:
641
+ - auto (calculates importance based on estimator's default implementation of feature importance;
642
+ estimator must be tree-based)
643
+ Note: if none provided, it uses lightgbm's LGBMRegressor as estimator, and "gain" as
644
+ importance type
645
+ - permutation (calculates importance based on mean decrease in accuracy when a feature column is permuted;
646
+ estimator can be any form)
647
+ Hint: for permutation, downsample data for better performance especially if X.shape[1] is large
648
+
649
+ Args:
650
+ X (np.matrix or np.array or pd.Dataframe): a feature matrix
651
+ tau (np.array): a treatment effect vector (estimated/actual)
652
+ model_tau_feature (sklearn/lightgbm/xgboost model object): an unfitted model object
653
+ features (np.array): list/array of feature names. If None, an enumerated list will be used
654
+ method (str): auto, permutation
655
+ normalize (bool): normalize by sum of importances if method=auto (defaults to True)
656
+ test_size (float/int): if float, represents the proportion of the dataset to include in the test split.
657
+ If int, represents the absolute number of test samples (used for estimating
658
+ permutation importance)
659
+ random_state (int/RandomState instance/None): random state used in permutation importance estimation
660
+ """
661
+ explainer = Explainer(
662
+ method=method,
663
+ control_name=self.control_name,
664
+ X=X,
665
+ tau=tau,
666
+ model_tau=model_tau_feature,
667
+ features=features,
668
+ classes=self._classes,
669
+ normalize=normalize,
670
+ test_size=test_size,
671
+ random_state=random_state,
672
+ )
673
+ return explainer.get_importance()
674
+
675
+ def get_shap_values(self, X=None, model_tau_feature=None, tau=None, features=None):
676
+ """
677
+ Builds a model (using X to predict estimated/actual tau), and then calculates shapley values.
678
+ Args:
679
+ X (np.matrix or np.array or pd.Dataframe): a feature matrix
680
+ tau (np.array): a treatment effect vector (estimated/actual)
681
+ model_tau_feature (sklearn/lightgbm/xgboost model object): an unfitted model object
682
+ features (optional, np.array): list/array of feature names. If None, an enumerated list will be used.
683
+ """
684
+ explainer = Explainer(
685
+ method="shapley",
686
+ control_name=self.control_name,
687
+ X=X,
688
+ tau=tau,
689
+ model_tau=model_tau_feature,
690
+ features=features,
691
+ classes=self._classes,
692
+ )
693
+ return explainer.get_shap_values()
694
+
695
+ def plot_importance(
696
+ self,
697
+ X=None,
698
+ tau=None,
699
+ model_tau_feature=None,
700
+ features=None,
701
+ method="auto",
702
+ normalize=True,
703
+ test_size=0.3,
704
+ random_state=None,
705
+ ):
706
+ """
707
+ Builds a model (using X to predict estimated/actual tau), and then plots feature importances
708
+ based on a specified method.
709
+
710
+ Currently supported methods are:
711
+ - auto (calculates importance based on estimator's default implementation of feature importance;
712
+ estimator must be tree-based)
713
+ Note: if none provided, it uses lightgbm's LGBMRegressor as estimator, and "gain" as
714
+ importance type
715
+ - permutation (calculates importance based on mean decrease in accuracy when a feature column is permuted;
716
+ estimator can be any form)
717
+ Hint: for permutation, downsample data for better performance especially if X.shape[1] is large
718
+
719
+ Args:
720
+ X (np.matrix or np.array or pd.Dataframe): a feature matrix
721
+ tau (np.array): a treatment effect vector (estimated/actual)
722
+ model_tau_feature (sklearn/lightgbm/xgboost model object): an unfitted model object
723
+ features (optional, np.array): list/array of feature names. If None, an enumerated list will be used
724
+ method (str): auto, permutation
725
+ normalize (bool): normalize by sum of importances if method=auto (defaults to True)
726
+ test_size (float/int): if float, represents the proportion of the dataset to include in the test split.
727
+ If int, represents the absolute number of test samples (used for estimating
728
+ permutation importance)
729
+ random_state (int/RandomState instance/None): random state used in permutation importance estimation
730
+ """
731
+ explainer = Explainer(
732
+ method=method,
733
+ control_name=self.control_name,
734
+ X=X,
735
+ tau=tau,
736
+ model_tau=model_tau_feature,
737
+ features=features,
738
+ classes=self._classes,
739
+ normalize=normalize,
740
+ test_size=test_size,
741
+ random_state=random_state,
742
+ )
743
+ explainer.plot_importance()
744
+
745
+ def plot_shap_values(
746
+ self,
747
+ X=None,
748
+ tau=None,
749
+ model_tau_feature=None,
750
+ features=None,
751
+ shap_dict=None,
752
+ **kwargs,
753
+ ):
754
+ """
755
+ Plots distribution of shapley values.
756
+
757
+ If shapley values have been pre-computed, pass it through the shap_dict parameter.
758
+ If shap_dict is not provided, this builds a new model (using X to predict estimated/actual tau),
759
+ and then calculates shapley values.
760
+
761
+ Args:
762
+ X (np.matrix or np.array or pd.Dataframe): a feature matrix. Required if shap_dict is None.
763
+ tau (np.array): a treatment effect vector (estimated/actual)
764
+ model_tau_feature (sklearn/lightgbm/xgboost model object): an unfitted model object
765
+ features (optional, np.array): list/array of feature names. If None, an enumerated list will be used.
766
+ shap_dict (optional, dict): a dict of shapley value matrices. If None, shap_dict will be computed.
767
+ """
768
+ override_checks = False if shap_dict is None else True
769
+ explainer = Explainer(
770
+ method="shapley",
771
+ control_name=self.control_name,
772
+ X=X,
773
+ tau=tau,
774
+ model_tau=model_tau_feature,
775
+ features=features,
776
+ override_checks=override_checks,
777
+ classes=self._classes,
778
+ )
779
+ explainer.plot_shap_values(shap_dict=shap_dict)
780
+
781
+ def plot_shap_dependence(
782
+ self,
783
+ treatment_group,
784
+ feature_idx,
785
+ X,
786
+ tau,
787
+ model_tau_feature=None,
788
+ features=None,
789
+ shap_dict=None,
790
+ interaction_idx="auto",
791
+ **kwargs,
792
+ ):
793
+ """
794
+ Plots dependency of shapley values for a specified feature, colored by an interaction feature.
795
+
796
+ If shapley values have been pre-computed, pass it through the shap_dict parameter.
797
+ If shap_dict is not provided, this builds a new model (using X to predict estimated/actual tau),
798
+ and then calculates shapley values.
799
+
800
+ This plots the value of the feature on the x-axis and the SHAP value of the same feature
801
+ on the y-axis. This shows how the model depends on the given feature, and is like a
802
+ richer extension of the classical partial dependence plots. Vertical dispersion of the
803
+ data points represents interaction effects.
804
+
805
+ Args:
806
+ treatment_group (str or int): name of treatment group to create dependency plot on
807
+ feature_idx (str or int): feature index / name to create dependency plot on
808
+ X (np.matrix or np.array or pd.Dataframe): a feature matrix
809
+ tau (np.array): a treatment effect vector (estimated/actual)
810
+ model_tau_feature (sklearn/lightgbm/xgboost model object): an unfitted model object
811
+ features (optional, np.array): list/array of feature names. If None, an enumerated list will be used.
812
+ shap_dict (optional, dict): a dict of shapley value matrices. If None, shap_dict will be computed.
813
+ interaction_idx (optional, str or int): feature index / name used in coloring scheme as interaction feature.
814
+ If "auto" then shap.common.approximate_interactions is used to pick what seems to be the
815
+ strongest interaction (note that to find to true strongest interaction you need to compute
816
+ the SHAP interaction values).
817
+ """
818
+ override_checks = False if shap_dict is None else True
819
+ explainer = Explainer(
820
+ method="shapley",
821
+ control_name=self.control_name,
822
+ X=X,
823
+ tau=tau,
824
+ model_tau=model_tau_feature,
825
+ features=features,
826
+ override_checks=override_checks,
827
+ classes=self._classes,
828
+ )
829
+ explainer.plot_shap_dependence(
830
+ treatment_group=treatment_group,
831
+ feature_idx=feature_idx,
832
+ shap_dict=shap_dict,
833
+ interaction_idx=interaction_idx,
834
+ **kwargs,
835
+ )
836
+
837
+
838
+ class BaseDRIVRegressor(BaseDRIVLearner):
839
+ """
840
+ A parent class for DRIV-learner regressor classes.
841
+ """
842
+
843
+ def __init__(
844
+ self,
845
+ learner=None,
846
+ control_outcome_learner=None,
847
+ treatment_outcome_learner=None,
848
+ treatment_effect_learner=None,
849
+ ate_alpha=0.05,
850
+ control_name=0,
851
+ ):
852
+ """Initialize a DRIV-learner regressor.
853
+
854
+ Args:
855
+ learner (optional): a model to estimate outcomes and treatment effects in both the control and treatment
856
+ groups
857
+ control_outcome_learner (optional): a model to estimate outcomes in the control group
858
+ treatment_outcome_learner (optional): a model to estimate outcomes in the treatment group
859
+ treatment_effect_learner (optional): a model to estimate treatment effects in the treatment group. It needs
860
+ to take `sample_weight` as an input argument in `fit()`.
861
+ ate_alpha (float, optional): the confidence level alpha of the ATE estimate
862
+ control_name (str or int, optional): name of control group
863
+ """
864
+ super().__init__(
865
+ learner=learner,
866
+ control_outcome_learner=control_outcome_learner,
867
+ treatment_outcome_learner=treatment_outcome_learner,
868
+ treatment_effect_learner=treatment_effect_learner,
869
+ ate_alpha=ate_alpha,
870
+ control_name=control_name,
871
+ )
872
+
873
+
874
+ class XGBDRIVRegressor(BaseDRIVRegressor):
875
+ def __init__(self, ate_alpha=0.05, control_name=0, *args, **kwargs):
876
+ """Initialize a DRIV-learner with two XGBoost models."""
877
+ super().__init__(
878
+ learner=XGBRegressor(*args, **kwargs),
879
+ ate_alpha=ate_alpha,
880
+ control_name=control_name,
881
+ )
causalml/source/causalml/inference/iv/iv_regression.py ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import numpy as np
2
+
3
+ from causalml.inference.meta.utils import convert_pd_to_np
4
+ import statsmodels.api as sm
5
+ from statsmodels.sandbox.regression.gmm import IV2SLS
6
+
7
+
8
+ class IVRegressor:
9
+ """A wrapper class that uses IV2SLS from statsmodel
10
+
11
+ A linear 2SLS model that estimates the average treatment effect with endogenous treatment variable.
12
+ """
13
+
14
+ def __init__(self):
15
+ """
16
+ Initializes the class.
17
+ """
18
+
19
+ self.method = "2SLS"
20
+
21
+ def fit(self, X, treatment, y, w):
22
+ """Fits the 2SLS model.
23
+
24
+ Args:
25
+ X (np.matrix or np.array or pd.Dataframe): a feature matrix
26
+ treatment (np.array or pd.Series): a treatment vector
27
+ y (np.array or pd.Series): an outcome vector
28
+ w (np.array or pd.Series): an instrument vector
29
+ """
30
+
31
+ X, treatment, y, w = convert_pd_to_np(X, treatment, y, w)
32
+
33
+ exog = sm.add_constant(np.c_[X, treatment])
34
+ endog = y
35
+ instrument = sm.add_constant(np.c_[X, w])
36
+
37
+ self.iv_model = IV2SLS(endog=endog, exog=exog, instrument=instrument)
38
+ self.iv_fit = self.iv_model.fit()
39
+
40
+ def predict(self):
41
+ """Returns the average treatment effect and its estimated standard error
42
+
43
+ Returns:
44
+ (float): average treatment effect
45
+ (float): standard error of the estimation
46
+ """
47
+
48
+ return self.iv_fit.params[-1], self.iv_fit.bse[-1]
causalml/source/causalml/inference/meta/__init__.py ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from .slearner import LRSRegressor, BaseSLearner, BaseSRegressor, BaseSClassifier
2
+ from .tlearner import (
3
+ XGBTRegressor,
4
+ MLPTRegressor,
5
+ BaseTLearner,
6
+ BaseTRegressor,
7
+ BaseTClassifier,
8
+ )
9
+ from .xlearner import BaseXLearner, BaseXRegressor, BaseXClassifier
10
+ from .rlearner import BaseRLearner, BaseRRegressor, BaseRClassifier, XGBRRegressor
11
+ from .tmle import TMLELearner
12
+ from .drlearner import BaseDRLearner, BaseDRRegressor, BaseDRClassifier, XGBDRRegressor
causalml/source/causalml/inference/meta/base.py ADDED
@@ -0,0 +1,337 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from abc import ABCMeta, abstractmethod
2
+ import logging
3
+ import numpy as np
4
+ import pandas as pd
5
+
6
+ from causalml.inference.meta.explainer import Explainer
7
+ from causalml.inference.meta.utils import check_p_conditions, convert_pd_to_np
8
+ from causalml.propensity import compute_propensity_score
9
+
10
+ logger = logging.getLogger("causalml")
11
+
12
+
13
+ class BaseLearner(metaclass=ABCMeta):
14
+ @classmethod
15
+ @abstractmethod
16
+ def fit(self, X, treatment, y, p=None):
17
+ pass
18
+
19
+ @classmethod
20
+ @abstractmethod
21
+ def predict(
22
+ self, X, treatment=None, y=None, p=None, return_components=False, verbose=True
23
+ ):
24
+ pass
25
+
26
+ def fit_predict(
27
+ self,
28
+ X,
29
+ treatment,
30
+ y,
31
+ p=None,
32
+ return_ci=False,
33
+ n_bootstraps=1000,
34
+ bootstrap_size=10000,
35
+ return_components=False,
36
+ verbose=True,
37
+ ):
38
+ self.fit(X, treatment, y, p)
39
+ return self.predict(X, treatment, y, p, return_components, verbose)
40
+
41
+ @classmethod
42
+ @abstractmethod
43
+ def estimate_ate(
44
+ self,
45
+ X,
46
+ treatment,
47
+ y,
48
+ p=None,
49
+ bootstrap_ci=False,
50
+ n_bootstraps=1000,
51
+ bootstrap_size=10000,
52
+ ):
53
+ pass
54
+
55
+ def bootstrap(self, X, treatment, y, p=None, size=10000):
56
+ """Runs a single bootstrap. Fits on bootstrapped sample, then predicts on whole population."""
57
+ idxs = np.random.choice(np.arange(0, X.shape[0]), size=size)
58
+ X_b = X[idxs]
59
+
60
+ if p is not None:
61
+ p_b = {group: _p[idxs] for group, _p in p.items()}
62
+ else:
63
+ p_b = None
64
+
65
+ treatment_b = treatment[idxs]
66
+ y_b = y[idxs]
67
+ self.fit(X=X_b, treatment=treatment_b, y=y_b, p=p_b)
68
+ return self.predict(X=X, p=p)
69
+
70
+ @staticmethod
71
+ def _format_p(p, t_groups):
72
+ """Format propensity scores into a dictionary of {treatment group: propensity scores}.
73
+
74
+ Args:
75
+ p (np.ndarray, pd.Series, or dict): propensity scores
76
+ t_groups (list): treatment group names.
77
+
78
+ Returns:
79
+ dict of {treatment group: propensity scores}
80
+ """
81
+ check_p_conditions(p, t_groups)
82
+
83
+ if isinstance(p, (np.ndarray, pd.Series)):
84
+ treatment_name = t_groups[0]
85
+ p = {treatment_name: convert_pd_to_np(p)}
86
+ elif isinstance(p, dict):
87
+ p = {
88
+ treatment_name: convert_pd_to_np(_p) for treatment_name, _p in p.items()
89
+ }
90
+
91
+ return p
92
+
93
+ def _set_propensity_models(self, X, treatment, y):
94
+ """Set self.propensity and self.propensity_models.
95
+
96
+ It trains propensity models for all treatment groups, save them in self.propensity_models, and
97
+ save propensity scores in self.propensity in dictionaries with treatment groups as keys.
98
+
99
+ It will use self.model_p if available to train propensity models. Otherwise, it will use a default
100
+ PropensityModel (i.e. ElasticNetPropensityModel).
101
+
102
+ Args:
103
+ X (np.matrix or np.array or pd.Dataframe): a feature matrix
104
+ treatment (np.array or pd.Series): a treatment vector
105
+ y (np.array or pd.Series): an outcome vector
106
+ """
107
+ logger.info("Generating propensity score")
108
+ p = dict()
109
+ p_model = dict()
110
+ for group in self.t_groups:
111
+ mask = (treatment == group) | (treatment == self.control_name)
112
+ treatment_filt = treatment[mask]
113
+ X_filt = X[mask]
114
+ w_filt = (treatment_filt == group).astype(int)
115
+ w = (treatment == group).astype(int)
116
+ propensity_model = self.model_p if hasattr(self, "model_p") else None
117
+ p[group], p_model[group] = compute_propensity_score(
118
+ X=X_filt,
119
+ treatment=w_filt,
120
+ p_model=propensity_model,
121
+ X_pred=X,
122
+ treatment_pred=w,
123
+ )
124
+ self.propensity_model = p_model
125
+ self.propensity = p
126
+
127
+ def get_importance(
128
+ self,
129
+ X=None,
130
+ tau=None,
131
+ model_tau_feature=None,
132
+ features=None,
133
+ method="auto",
134
+ normalize=True,
135
+ test_size=0.3,
136
+ random_state=None,
137
+ ):
138
+ """
139
+ Builds a model (using X to predict estimated/actual tau), and then calculates feature importances
140
+ based on a specified method.
141
+
142
+ Currently supported methods are:
143
+ - auto (calculates importance based on estimator's default implementation of feature importance;
144
+ estimator must be tree-based)
145
+ Note: if none provided, it uses lightgbm's LGBMRegressor as estimator, and "gain" as
146
+ importance type
147
+ - permutation (calculates importance based on mean decrease in accuracy when a feature column is permuted;
148
+ estimator can be any form)
149
+ Hint: for permutation, downsample data for better performance especially if X.shape[1] is large
150
+
151
+ Args:
152
+ X (np.matrix or np.array or pd.Dataframe): a feature matrix
153
+ tau (np.array): a treatment effect vector (estimated/actual)
154
+ model_tau_feature (sklearn/lightgbm/xgboost model object): an unfitted model object
155
+ features (np.array): list/array of feature names. If None, an enumerated list will be used
156
+ method (str): auto, permutation
157
+ normalize (bool): normalize by sum of importances if method=auto (defaults to True)
158
+ test_size (float/int): if float, represents the proportion of the dataset to include in the test split.
159
+ If int, represents the absolute number of test samples (used for estimating
160
+ permutation importance)
161
+ random_state (int/RandomState instance/None): random state used in permutation importance estimation
162
+ """
163
+ explainer = Explainer(
164
+ method=method,
165
+ control_name=self.control_name,
166
+ X=X,
167
+ tau=tau,
168
+ model_tau=model_tau_feature,
169
+ features=features,
170
+ classes=self._classes,
171
+ normalize=normalize,
172
+ test_size=test_size,
173
+ random_state=random_state,
174
+ )
175
+ return explainer.get_importance()
176
+
177
+ def get_shap_values(self, X=None, model_tau_feature=None, tau=None, features=None):
178
+ """
179
+ Builds a model (using X to predict estimated/actual tau), and then calculates shapley values.
180
+ Args:
181
+ X (np.matrix or np.array or pd.Dataframe): a feature matrix
182
+ tau (np.array): a treatment effect vector (estimated/actual)
183
+ model_tau_feature (sklearn/lightgbm/xgboost model object): an unfitted model object
184
+ features (optional, np.array): list/array of feature names. If None, an enumerated list will be used.
185
+ """
186
+ explainer = Explainer(
187
+ method="shapley",
188
+ control_name=self.control_name,
189
+ X=X,
190
+ tau=tau,
191
+ model_tau=model_tau_feature,
192
+ features=features,
193
+ classes=self._classes,
194
+ )
195
+ return explainer.get_shap_values()
196
+
197
+ def plot_importance(
198
+ self,
199
+ X=None,
200
+ tau=None,
201
+ model_tau_feature=None,
202
+ features=None,
203
+ method="auto",
204
+ normalize=True,
205
+ test_size=0.3,
206
+ random_state=None,
207
+ ):
208
+ """
209
+ Builds a model (using X to predict estimated/actual tau), and then plots feature importances
210
+ based on a specified method.
211
+
212
+ Currently supported methods are:
213
+ - auto (calculates importance based on estimator's default implementation of feature importance;
214
+ estimator must be tree-based)
215
+ Note: if none provided, it uses lightgbm's LGBMRegressor as estimator, and "gain" as
216
+ importance type
217
+ - permutation (calculates importance based on mean decrease in accuracy when a feature column is permuted;
218
+ estimator can be any form)
219
+ Hint: for permutation, downsample data for better performance especially if X.shape[1] is large
220
+
221
+ Args:
222
+ X (np.matrix or np.array or pd.Dataframe): a feature matrix
223
+ tau (np.array): a treatment effect vector (estimated/actual)
224
+ model_tau_feature (sklearn/lightgbm/xgboost model object): an unfitted model object
225
+ features (optional, np.array): list/array of feature names. If None, an enumerated list will be used
226
+ method (str): auto, permutation
227
+ normalize (bool): normalize by sum of importances if method=auto (defaults to True)
228
+ test_size (float/int): if float, represents the proportion of the dataset to include in the test split.
229
+ If int, represents the absolute number of test samples (used for estimating
230
+ permutation importance)
231
+ random_state (int/RandomState instance/None): random state used in permutation importance estimation
232
+ """
233
+ explainer = Explainer(
234
+ method=method,
235
+ control_name=self.control_name,
236
+ X=X,
237
+ tau=tau,
238
+ model_tau=model_tau_feature,
239
+ features=features,
240
+ classes=self._classes,
241
+ normalize=normalize,
242
+ test_size=test_size,
243
+ random_state=random_state,
244
+ )
245
+ explainer.plot_importance()
246
+
247
+ def plot_shap_values(
248
+ self,
249
+ X=None,
250
+ tau=None,
251
+ model_tau_feature=None,
252
+ features=None,
253
+ shap_dict=None,
254
+ **kwargs,
255
+ ):
256
+ """
257
+ Plots distribution of shapley values.
258
+
259
+ If shapley values have been pre-computed, pass it through the shap_dict parameter.
260
+ If shap_dict is not provided, this builds a new model (using X to predict estimated/actual tau),
261
+ and then calculates shapley values.
262
+
263
+ Args:
264
+ X (np.matrix or np.array or pd.Dataframe): a feature matrix. Required if shap_dict is None.
265
+ tau (np.array): a treatment effect vector (estimated/actual)
266
+ model_tau_feature (sklearn/lightgbm/xgboost model object): an unfitted model object
267
+ features (optional, np.array): list/array of feature names. If None, an enumerated list will be used.
268
+ shap_dict (optional, dict): a dict of shapley value matrices. If None, shap_dict will be computed.
269
+ """
270
+ override_checks = shap_dict is not None
271
+ explainer = Explainer(
272
+ method="shapley",
273
+ control_name=self.control_name,
274
+ X=X,
275
+ tau=tau,
276
+ model_tau=model_tau_feature,
277
+ features=features,
278
+ override_checks=override_checks,
279
+ classes=self._classes,
280
+ )
281
+ explainer.plot_shap_values(shap_dict=shap_dict, **kwargs)
282
+
283
+ def plot_shap_dependence(
284
+ self,
285
+ treatment_group,
286
+ feature_idx,
287
+ X,
288
+ tau,
289
+ model_tau_feature=None,
290
+ features=None,
291
+ shap_dict=None,
292
+ interaction_idx="auto",
293
+ **kwargs,
294
+ ):
295
+ """
296
+ Plots dependency of shapley values for a specified feature, colored by an interaction feature.
297
+
298
+ If shapley values have been pre-computed, pass it through the shap_dict parameter.
299
+ If shap_dict is not provided, this builds a new model (using X to predict estimated/actual tau),
300
+ and then calculates shapley values.
301
+
302
+ This plots the value of the feature on the x-axis and the SHAP value of the same feature
303
+ on the y-axis. This shows how the model depends on the given feature, and is like a
304
+ richer extension of the classical partial dependence plots. Vertical dispersion of the
305
+ data points represents interaction effects.
306
+
307
+ Args:
308
+ treatment_group (str or int): name of treatment group to create dependency plot on
309
+ feature_idx (str or int): feature index / name to create dependency plot on
310
+ X (np.matrix or np.array or pd.Dataframe): a feature matrix
311
+ tau (np.array): a treatment effect vector (estimated/actual)
312
+ model_tau_feature (sklearn/lightgbm/xgboost model object): an unfitted model object
313
+ features (optional, np.array): list/array of feature names. If None, an enumerated list will be used.
314
+ shap_dict (optional, dict): a dict of shapley value matrices. If None, shap_dict will be computed.
315
+ interaction_idx (optional, str or int): feature index / name used in coloring scheme as interaction feature.
316
+ If "auto" then shap.common.approximate_interactions is used to pick what seems to be the
317
+ strongest interaction (note that to find to true strongest interaction you need to compute
318
+ the SHAP interaction values).
319
+ """
320
+ override_checks = False if shap_dict is None else True
321
+ explainer = Explainer(
322
+ method="shapley",
323
+ control_name=self.control_name,
324
+ X=X,
325
+ tau=tau,
326
+ model_tau=model_tau_feature,
327
+ features=features,
328
+ override_checks=override_checks,
329
+ classes=self._classes,
330
+ )
331
+ explainer.plot_shap_dependence(
332
+ treatment_group=treatment_group,
333
+ feature_idx=feature_idx,
334
+ shap_dict=shap_dict,
335
+ interaction_idx=interaction_idx,
336
+ **kwargs,
337
+ )
causalml/source/causalml/inference/meta/drlearner.py ADDED
@@ -0,0 +1,592 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from copy import deepcopy
2
+ import logging
3
+ import numpy as np
4
+ import pandas as pd
5
+ from scipy.stats import norm
6
+ from sklearn.model_selection import KFold
7
+ from tqdm import tqdm
8
+ from xgboost import XGBRegressor
9
+
10
+ from causalml.inference.meta.base import BaseLearner
11
+ from causalml.inference.meta.utils import (
12
+ check_treatment_vector,
13
+ check_p_conditions,
14
+ convert_pd_to_np,
15
+ )
16
+ from causalml.metrics import regression_metrics, classification_metrics
17
+ from causalml.propensity import compute_propensity_score
18
+
19
+ logger = logging.getLogger("causalml")
20
+
21
+
22
+ class BaseDRLearner(BaseLearner):
23
+ """A parent class for DR-learner regressor classes.
24
+
25
+ A DR-learner estimates treatment effects with machine learning models.
26
+
27
+ Details of DR-learner are available at `Kennedy (2020) <https://arxiv.org/abs/2004.14497>`_.
28
+ """
29
+
30
+ def __init__(
31
+ self,
32
+ learner=None,
33
+ control_outcome_learner=None,
34
+ treatment_outcome_learner=None,
35
+ treatment_effect_learner=None,
36
+ ate_alpha=0.05,
37
+ control_name=0,
38
+ ):
39
+ """Initialize a DR-learner.
40
+
41
+ Args:
42
+ learner (optional): a model to estimate outcomes and treatment effects in both the control and treatment
43
+ groups
44
+ control_outcome_learner (optional): a model to estimate outcomes in the control group
45
+ treatment_outcome_learner (optional): a model to estimate outcomes in the treatment group
46
+ treatment_effect_learner (optional): a model to estimate treatment effects in the treatment group
47
+ ate_alpha (float, optional): the confidence level alpha of the ATE estimate
48
+ control_name (str or int, optional): name of control group
49
+ """
50
+ assert (learner is not None) or (
51
+ (control_outcome_learner is not None)
52
+ and (treatment_outcome_learner is not None)
53
+ and (treatment_effect_learner is not None)
54
+ )
55
+
56
+ if control_outcome_learner is None:
57
+ self.model_mu_c = deepcopy(learner)
58
+ else:
59
+ self.model_mu_c = control_outcome_learner
60
+
61
+ if treatment_outcome_learner is None:
62
+ self.model_mu_t = deepcopy(learner)
63
+ else:
64
+ self.model_mu_t = treatment_outcome_learner
65
+
66
+ if treatment_effect_learner is None:
67
+ self.model_tau = deepcopy(learner)
68
+ else:
69
+ self.model_tau = treatment_effect_learner
70
+
71
+ self.ate_alpha = ate_alpha
72
+ self.control_name = control_name
73
+
74
+ self.propensity = None
75
+
76
+ def __repr__(self):
77
+ return (
78
+ "{}(control_outcome_learner={},\n"
79
+ "\ttreatment_outcome_learner={},\n"
80
+ "\ttreatment_effect_learner={})".format(
81
+ self.__class__.__name__,
82
+ self.model_mu_c.__repr__(),
83
+ self.model_mu_t.__repr__(),
84
+ self.model_tau.__repr__(),
85
+ )
86
+ )
87
+
88
+ def fit(self, X, treatment, y, p=None, seed=None):
89
+ """Fit the inference model.
90
+
91
+ Args:
92
+ X (np.matrix or np.array or pd.Dataframe): a feature matrix
93
+ treatment (np.array or pd.Series): a treatment vector
94
+ y (np.array or pd.Series): an outcome vector
95
+ p (np.ndarray or pd.Series or dict, optional): an array of propensity scores of float (0,1) in the
96
+ single-treatment case; or, a dictionary of treatment groups that map to propensity vectors of
97
+ float (0,1); if None will run ElasticNetPropensityModel() to generate the propensity scores.
98
+ seed (int): random seed for cross-fitting
99
+ """
100
+ X, treatment, y = convert_pd_to_np(X, treatment, y)
101
+ check_treatment_vector(treatment, self.control_name)
102
+ self.t_groups = np.unique(treatment[treatment != self.control_name])
103
+ self.t_groups.sort()
104
+ self._classes = {group: i for i, group in enumerate(self.t_groups)}
105
+
106
+ # The estimator splits the data into 3 partitions for cross-fit on the propensity score estimation,
107
+ # the outcome regression, and the treatment regression on the doubly robust estimates. The use of
108
+ # the partitions is rotated so we do not lose on the sample size.
109
+ cv = KFold(n_splits=3, shuffle=True, random_state=seed)
110
+ split_indices = [index for _, index in cv.split(y)]
111
+
112
+ self.models_mu_c = [
113
+ deepcopy(self.model_mu_c),
114
+ deepcopy(self.model_mu_c),
115
+ deepcopy(self.model_mu_c),
116
+ ]
117
+ self.models_mu_t = {
118
+ group: [
119
+ deepcopy(self.model_mu_t),
120
+ deepcopy(self.model_mu_t),
121
+ deepcopy(self.model_mu_t),
122
+ ]
123
+ for group in self.t_groups
124
+ }
125
+ self.models_tau = {
126
+ group: [
127
+ deepcopy(self.model_tau),
128
+ deepcopy(self.model_tau),
129
+ deepcopy(self.model_tau),
130
+ ]
131
+ for group in self.t_groups
132
+ }
133
+ if p is None:
134
+ self.propensity = {group: np.zeros(y.shape[0]) for group in self.t_groups}
135
+
136
+ for ifold in range(3):
137
+ treatment_idx = split_indices[ifold]
138
+ outcome_idx = split_indices[(ifold + 1) % 3]
139
+ tau_idx = split_indices[(ifold + 2) % 3]
140
+
141
+ treatment_treat, treatment_out, treatment_tau = (
142
+ treatment[treatment_idx],
143
+ treatment[outcome_idx],
144
+ treatment[tau_idx],
145
+ )
146
+ y_out, y_tau = y[outcome_idx], y[tau_idx]
147
+ X_treat, X_out, X_tau = X[treatment_idx], X[outcome_idx], X[tau_idx]
148
+
149
+ if p is None:
150
+ logger.info("Generating propensity score")
151
+ cur_p = dict()
152
+
153
+ for group in self.t_groups:
154
+ mask = (treatment_treat == group) | (
155
+ treatment_treat == self.control_name
156
+ )
157
+ treatment_filt = treatment_treat[mask]
158
+ X_filt = X_treat[mask]
159
+ w_filt = (treatment_filt == group).astype(int)
160
+ w = (treatment_tau == group).astype(int)
161
+ cur_p[group], _ = compute_propensity_score(
162
+ X=X_filt, treatment=w_filt, X_pred=X_tau, treatment_pred=w
163
+ )
164
+ self.propensity[group][tau_idx] = cur_p[group]
165
+ else:
166
+ cur_p = dict()
167
+ if isinstance(p, (np.ndarray, pd.Series)):
168
+ cur_p = {self.t_groups[0]: convert_pd_to_np(p[tau_idx])}
169
+ else:
170
+ cur_p = {g: prop[tau_idx] for g, prop in p.items()}
171
+ check_p_conditions(cur_p, self.t_groups)
172
+
173
+ logger.info("Generate outcome regressions")
174
+ self.models_mu_c[ifold].fit(
175
+ X_out[treatment_out == self.control_name],
176
+ y_out[treatment_out == self.control_name],
177
+ )
178
+ for group in self.t_groups:
179
+ self.models_mu_t[group][ifold].fit(
180
+ X_out[treatment_out == group], y_out[treatment_out == group]
181
+ )
182
+
183
+ logger.info("Fit pseudo outcomes from the DR formula")
184
+
185
+ for group in self.t_groups:
186
+ mask = (treatment_tau == group) | (treatment_tau == self.control_name)
187
+ treatment_filt = treatment_tau[mask]
188
+ X_filt = X_tau[mask]
189
+ y_filt = y_tau[mask]
190
+ w_filt = (treatment_filt == group).astype(int)
191
+ p_filt = cur_p[group][mask]
192
+ mu_t = self.models_mu_t[group][ifold].predict(X_filt)
193
+ mu_c = self.models_mu_c[ifold].predict(X_filt)
194
+ dr = (
195
+ (w_filt - p_filt)
196
+ / p_filt
197
+ / (1 - p_filt)
198
+ * (y_filt - mu_t * w_filt - mu_c * (1 - w_filt))
199
+ + mu_t
200
+ - mu_c
201
+ )
202
+ self.models_tau[group][ifold].fit(X_filt, dr)
203
+
204
+ def predict(
205
+ self, X, treatment=None, y=None, p=None, return_components=False, verbose=True
206
+ ):
207
+ """Predict treatment effects.
208
+
209
+ Args:
210
+ X (np.matrix or np.array or pd.Dataframe): a feature matrix
211
+ treatment (np.array or pd.Series, optional): a treatment vector
212
+ y (np.array or pd.Series, optional): an outcome vector
213
+ verbose (bool, optional): whether to output progress logs
214
+ Returns:
215
+ (numpy.ndarray): Predictions of treatment effects.
216
+ """
217
+ X, treatment, y = convert_pd_to_np(X, treatment, y)
218
+
219
+ te = np.zeros((X.shape[0], self.t_groups.shape[0]))
220
+ yhat_cs = {}
221
+ yhat_ts = {}
222
+
223
+ for i, group in enumerate(self.t_groups):
224
+ models_tau = self.models_tau[group]
225
+ _te = np.r_[[model.predict(X) for model in models_tau]].mean(axis=0)
226
+ te[:, i] = np.ravel(_te)
227
+ yhat_cs[group] = np.r_[
228
+ [model.predict(X) for model in self.models_mu_c]
229
+ ].mean(axis=0)
230
+ yhat_ts[group] = np.r_[
231
+ [model.predict(X) for model in self.models_mu_t[group]]
232
+ ].mean(axis=0)
233
+
234
+ if (y is not None) and (treatment is not None) and verbose:
235
+ mask = (treatment == group) | (treatment == self.control_name)
236
+ treatment_filt = treatment[mask]
237
+ y_filt = y[mask]
238
+ w = (treatment_filt == group).astype(int)
239
+
240
+ yhat = np.zeros_like(y_filt, dtype=float)
241
+ yhat[w == 0] = yhat_cs[group][mask][w == 0]
242
+ yhat[w == 1] = yhat_ts[group][mask][w == 1]
243
+
244
+ logger.info("Error metrics for group {}".format(group))
245
+ regression_metrics(y_filt, yhat, w)
246
+
247
+ if not return_components:
248
+ return te
249
+ else:
250
+ return te, yhat_cs, yhat_ts
251
+
252
+ def fit_predict(
253
+ self,
254
+ X,
255
+ treatment,
256
+ y,
257
+ p=None,
258
+ return_ci=False,
259
+ n_bootstraps=1000,
260
+ bootstrap_size=10000,
261
+ return_components=False,
262
+ verbose=True,
263
+ seed=None,
264
+ ):
265
+ """Fit the treatment effect and outcome models of the R learner and predict treatment effects.
266
+
267
+ Args:
268
+ X (np.matrix or np.array or pd.Dataframe): a feature matrix
269
+ treatment (np.array or pd.Series): a treatment vector
270
+ y (np.array or pd.Series): an outcome vector
271
+ p (np.ndarray or pd.Series or dict, optional): an array of propensity scores of float (0,1) in the
272
+ single-treatment case; or, a dictionary of treatment groups that map to propensity vectors of
273
+ float (0,1); if None will run ElasticNetPropensityModel() to generate the propensity scores.
274
+ return_ci (bool): whether to return confidence intervals
275
+ n_bootstraps (int): number of bootstrap iterations
276
+ bootstrap_size (int): number of samples per bootstrap
277
+ return_components (bool, optional): whether to return outcome for treatment and control seperately
278
+ verbose (str): whether to output progress logs
279
+ seed (int): random seed for cross-fitting
280
+ Returns:
281
+ (numpy.ndarray): Predictions of treatment effects. Output dim: [n_samples, n_treatment]
282
+ If return_ci, returns CATE [n_samples, n_treatment], LB [n_samples, n_treatment],
283
+ UB [n_samples, n_treatment]
284
+ """
285
+ X, treatment, y = convert_pd_to_np(X, treatment, y)
286
+ self.fit(X, treatment, y, p, seed)
287
+
288
+ if p is None:
289
+ p = self.propensity
290
+
291
+ check_p_conditions(p, self.t_groups)
292
+ if isinstance(p, (np.ndarray, pd.Series)):
293
+ treatment_name = self.t_groups[0]
294
+ p = {treatment_name: convert_pd_to_np(p)}
295
+ elif isinstance(p, dict):
296
+ p = {
297
+ treatment_name: convert_pd_to_np(_p) for treatment_name, _p in p.items()
298
+ }
299
+
300
+ te = self.predict(
301
+ X, treatment=treatment, y=y, return_components=return_components
302
+ )
303
+
304
+ if not return_ci:
305
+ return te
306
+ else:
307
+ t_groups_global = self.t_groups
308
+ _classes_global = self._classes
309
+ models_mu_c_global = deepcopy(self.models_mu_c)
310
+ models_mu_t_global = deepcopy(self.models_mu_t)
311
+ models_tau_global = deepcopy(self.models_tau)
312
+ te_bootstraps = np.zeros(
313
+ shape=(X.shape[0], self.t_groups.shape[0], n_bootstraps)
314
+ )
315
+
316
+ logger.info("Bootstrap Confidence Intervals")
317
+ for i in tqdm(range(n_bootstraps)):
318
+ te_b = self.bootstrap(X, treatment, y, p, size=bootstrap_size)
319
+ te_bootstraps[:, :, i] = te_b
320
+
321
+ te_lower = np.percentile(te_bootstraps, (self.ate_alpha / 2) * 100, axis=2)
322
+ te_upper = np.percentile(
323
+ te_bootstraps, (1 - self.ate_alpha / 2) * 100, axis=2
324
+ )
325
+
326
+ # set member variables back to global (currently last bootstrapped outcome)
327
+ self.t_groups = t_groups_global
328
+ self._classes = _classes_global
329
+ self.models_mu_c = deepcopy(models_mu_c_global)
330
+ self.models_mu_t = deepcopy(models_mu_t_global)
331
+ self.models_tau = deepcopy(models_tau_global)
332
+
333
+ return (te, te_lower, te_upper)
334
+
335
+ def estimate_ate(
336
+ self,
337
+ X,
338
+ treatment,
339
+ y,
340
+ p=None,
341
+ bootstrap_ci=False,
342
+ n_bootstraps=1000,
343
+ bootstrap_size=10000,
344
+ seed=None,
345
+ pretrain=False,
346
+ ):
347
+ """Estimate the Average Treatment Effect (ATE).
348
+
349
+ Args:
350
+ X (np.matrix or np.array or pd.Dataframe): a feature matrix
351
+ treatment (np.array or pd.Series): a treatment vector
352
+ y (np.array or pd.Series): an outcome vector
353
+ p (np.ndarray or pd.Series or dict, optional): an array of propensity scores of float (0,1) in the
354
+ single-treatment case; or, a dictionary of treatment groups that map to propensity vectors of
355
+ float (0,1); if None will run ElasticNetPropensityModel() to generate the propensity scores.
356
+ bootstrap_ci (bool): whether run bootstrap for confidence intervals
357
+ n_bootstraps (int): number of bootstrap iterations
358
+ bootstrap_size (int): number of samples per bootstrap
359
+ seed (int): random seed for cross-fitting
360
+ pretrain (bool): whether a model has been fit, default False.
361
+ Returns:
362
+ The mean and confidence interval (LB, UB) of the ATE estimate.
363
+ """
364
+ if pretrain:
365
+ te, yhat_cs, yhat_ts = self.predict(
366
+ X, treatment, y, p, return_components=True
367
+ )
368
+ else:
369
+ te, yhat_cs, yhat_ts = self.fit_predict(
370
+ X, treatment, y, p, return_components=True, seed=seed
371
+ )
372
+ X, treatment, y = convert_pd_to_np(X, treatment, y)
373
+
374
+ if p is None:
375
+ p = self.propensity
376
+ else:
377
+ check_p_conditions(p, self.t_groups)
378
+ if isinstance(p, (np.ndarray, pd.Series)):
379
+ treatment_name = self.t_groups[0]
380
+ p = {treatment_name: convert_pd_to_np(p)}
381
+ elif isinstance(p, dict):
382
+ p = {
383
+ treatment_name: convert_pd_to_np(_p) for treatment_name, _p in p.items()
384
+ }
385
+
386
+ ate = np.zeros(self.t_groups.shape[0])
387
+ ate_lb = np.zeros(self.t_groups.shape[0])
388
+ ate_ub = np.zeros(self.t_groups.shape[0])
389
+
390
+ for i, group in enumerate(self.t_groups):
391
+ _ate = te[:, i].mean()
392
+
393
+ mask = (treatment == group) | (treatment == self.control_name)
394
+ treatment_filt = treatment[mask]
395
+ w = (treatment_filt == group).astype(int)
396
+ prob_treatment = float(sum(w)) / w.shape[0]
397
+
398
+ yhat_c = yhat_cs[group][mask]
399
+ yhat_t = yhat_ts[group][mask]
400
+ y_filt = y[mask]
401
+
402
+ # SE formula is based on the lower bound formula (7) from Imbens, Guido W., and Jeffrey M. Wooldridge. 2009.
403
+ # "Recent Developments in the Econometrics of Program Evaluation." Journal of Economic Literature
404
+ se = np.sqrt(
405
+ (
406
+ (y_filt[w == 0] - yhat_c[w == 0]).var() / (1 - prob_treatment)
407
+ + (y_filt[w == 1] - yhat_t[w == 1]).var() / prob_treatment
408
+ + (yhat_t - yhat_c).var()
409
+ )
410
+ / y_filt.shape[0]
411
+ )
412
+
413
+ _ate_lb = _ate - se * norm.ppf(1 - self.ate_alpha / 2)
414
+ _ate_ub = _ate + se * norm.ppf(1 - self.ate_alpha / 2)
415
+
416
+ ate[i] = _ate
417
+ ate_lb[i] = _ate_lb
418
+ ate_ub[i] = _ate_ub
419
+
420
+ if not bootstrap_ci:
421
+ return ate, ate_lb, ate_ub
422
+ else:
423
+ t_groups_global = self.t_groups
424
+ _classes_global = self._classes
425
+ models_mu_c_global = deepcopy(self.models_mu_c)
426
+ models_mu_t_global = deepcopy(self.models_mu_t)
427
+ models_tau_global = deepcopy(self.models_tau)
428
+
429
+ logger.info("Bootstrap Confidence Intervals for ATE")
430
+ ate_bootstraps = np.zeros(shape=(self.t_groups.shape[0], n_bootstraps))
431
+
432
+ for n in tqdm(range(n_bootstraps)):
433
+ cate_b = self.bootstrap(
434
+ X, treatment, y, p, size=bootstrap_size, seed=seed
435
+ )
436
+ ate_bootstraps[:, n] = cate_b.mean(axis=0)
437
+
438
+ ate_lower = np.percentile(
439
+ ate_bootstraps, (self.ate_alpha / 2) * 100, axis=1
440
+ )
441
+ ate_upper = np.percentile(
442
+ ate_bootstraps, (1 - self.ate_alpha / 2) * 100, axis=1
443
+ )
444
+
445
+ # set member variables back to global (currently last bootstrapped outcome)
446
+ self.t_groups = t_groups_global
447
+ self._classes = _classes_global
448
+ self.models_mu_c = deepcopy(models_mu_c_global)
449
+ self.models_mu_t = deepcopy(models_mu_t_global)
450
+ self.models_tau = deepcopy(models_tau_global)
451
+ return ate, ate_lower, ate_upper
452
+
453
+
454
+ class BaseDRRegressor(BaseDRLearner):
455
+ """
456
+ A parent class for DR-learner regressor classes.
457
+ """
458
+
459
+ def __init__(
460
+ self,
461
+ learner=None,
462
+ control_outcome_learner=None,
463
+ treatment_outcome_learner=None,
464
+ treatment_effect_learner=None,
465
+ ate_alpha=0.05,
466
+ control_name=0,
467
+ ):
468
+ """Initialize an DR-learner regressor.
469
+
470
+ Args:
471
+ learner (optional): a model to estimate outcomes and treatment effects in both the control and treatment
472
+ groups
473
+ control_outcome_learner (optional): a model to estimate outcomes in the control group
474
+ treatment_outcome_learner (optional): a model to estimate outcomes in the treatment group
475
+ treatment_effect_learner (optional): a model to estimate treatment effects in the treatment group
476
+ ate_alpha (float, optional): the confidence level alpha of the ATE estimate
477
+ control_name (str or int, optional): name of control group
478
+ """
479
+ super().__init__(
480
+ learner=learner,
481
+ control_outcome_learner=control_outcome_learner,
482
+ treatment_outcome_learner=treatment_outcome_learner,
483
+ treatment_effect_learner=treatment_effect_learner,
484
+ ate_alpha=ate_alpha,
485
+ control_name=control_name,
486
+ )
487
+
488
+
489
+ class BaseDRClassifier(BaseDRLearner):
490
+ """
491
+ A parent class for DR-learner classifier classes.
492
+ """
493
+
494
+ def __init__(
495
+ self,
496
+ learner=None,
497
+ control_outcome_learner=None,
498
+ treatment_outcome_learner=None,
499
+ treatment_effect_learner=None,
500
+ ate_alpha=0.05,
501
+ control_name=0,
502
+ ):
503
+ """Initialize a DR-learner classifier.
504
+
505
+ Args:
506
+ learner (optional): a model to estimate outcomes and treatment effects in both the control and treatment
507
+ groups. Should have a predict_proba() method for outcome models.
508
+ control_outcome_learner (optional): a model to estimate outcomes in the control group.
509
+ Should have a predict_proba() method.
510
+ treatment_outcome_learner (optional): a model to estimate outcomes in the treatment group.
511
+ Should have a predict_proba() method.
512
+ treatment_effect_learner (optional): a model to estimate treatment effects in the treatment group.
513
+ Should be a regressor.
514
+ ate_alpha (float, optional): the confidence level alpha of the ATE estimate
515
+ control_name (str or int, optional): name of control group
516
+ """
517
+ super().__init__(
518
+ learner=learner,
519
+ control_outcome_learner=control_outcome_learner,
520
+ treatment_outcome_learner=treatment_outcome_learner,
521
+ treatment_effect_learner=treatment_effect_learner,
522
+ ate_alpha=ate_alpha,
523
+ control_name=control_name,
524
+ )
525
+
526
+ def predict(
527
+ self, X, treatment=None, y=None, p=None, return_components=False, verbose=True
528
+ ):
529
+ """Predict treatment effects.
530
+
531
+ Args:
532
+ X (np.matrix or np.array or pd.Dataframe): a feature matrix
533
+ treatment (np.array or pd.Series, optional): a treatment vector. Used for computing
534
+ classification metrics when y is also provided.
535
+ y (np.array or pd.Series, optional): an outcome vector. Used for computing
536
+ classification metrics when treatment is also provided.
537
+ p (np.ndarray or pd.Series or dict, optional): an array of propensity scores of float (0,1) in the
538
+ single-treatment case; or, a dictionary of treatment groups that map to propensity vectors of
539
+ float (0,1). Currently not used in prediction but kept for API consistency.
540
+ return_components (bool, optional): whether to return outcome probabilities for treatment and control
541
+ groups separately. Defaults to False.
542
+ verbose (bool, optional): whether to output progress logs. Defaults to True.
543
+ Returns:
544
+ (numpy.ndarray): Predictions of treatment effects.
545
+ If return_components is True, also returns:
546
+ - dict: Predicted probabilities for the control group (yhat_cs).
547
+ - dict: Predicted probabilities for the treatment group (yhat_ts).
548
+ """
549
+ X, treatment, y = convert_pd_to_np(X, treatment, y)
550
+
551
+ te = np.zeros((X.shape[0], self.t_groups.shape[0]))
552
+ yhat_cs = {}
553
+ yhat_ts = {}
554
+
555
+ for i, group in enumerate(self.t_groups):
556
+ models_tau = self.models_tau[group]
557
+ _te = np.r_[[model.predict(X) for model in models_tau]].mean(axis=0)
558
+ te[:, i] = np.ravel(_te)
559
+ yhat_cs[group] = np.r_[
560
+ [model.predict_proba(X)[:, 1] for model in self.models_mu_c]
561
+ ].mean(axis=0)
562
+ yhat_ts[group] = np.r_[
563
+ [model.predict_proba(X)[:, 1] for model in self.models_mu_t[group]]
564
+ ].mean(axis=0)
565
+
566
+ if (y is not None) and (treatment is not None) and verbose:
567
+ mask = (treatment == group) | (treatment == self.control_name)
568
+ treatment_filt = treatment[mask]
569
+ y_filt = y[mask]
570
+ w = (treatment_filt == group).astype(int)
571
+
572
+ yhat = np.zeros_like(y_filt, dtype=float)
573
+ yhat[w == 0] = yhat_cs[group][mask][w == 0]
574
+ yhat[w == 1] = yhat_ts[group][mask][w == 1]
575
+
576
+ logger.info("Error metrics for group {}".format(group))
577
+ classification_metrics(y_filt, yhat, w)
578
+
579
+ if not return_components:
580
+ return te
581
+ else:
582
+ return te, yhat_cs, yhat_ts
583
+
584
+
585
+ class XGBDRRegressor(BaseDRRegressor):
586
+ def __init__(self, ate_alpha=0.05, control_name=0, *args, **kwargs):
587
+ """Initialize a DR-learner with two XGBoost models."""
588
+ super().__init__(
589
+ learner=XGBRegressor(*args, **kwargs),
590
+ ate_alpha=ate_alpha,
591
+ control_name=control_name,
592
+ )
causalml/source/causalml/inference/meta/explainer.py ADDED
@@ -0,0 +1,278 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pandas as pd
2
+ import shap
3
+ import matplotlib.pyplot as plt
4
+ from lightgbm import LGBMRegressor
5
+ from sklearn.inspection import permutation_importance
6
+ from sklearn.model_selection import train_test_split
7
+ from copy import deepcopy
8
+
9
+ from causalml.inference.meta.utils import convert_pd_to_np
10
+
11
+ VALID_METHODS = ("auto", "permutation", "shapley")
12
+
13
+
14
+ class Explainer:
15
+ def __init__(
16
+ self,
17
+ method,
18
+ control_name,
19
+ X,
20
+ tau,
21
+ classes,
22
+ model_tau=None,
23
+ features=None,
24
+ normalize=True,
25
+ test_size=0.3,
26
+ random_state=None,
27
+ override_checks=False,
28
+ r_learners=None,
29
+ ):
30
+ """
31
+ The Explainer class handles all feature explanation/interpretation functions, including plotting
32
+ feature importances, shapley value distributions, and shapley value dependency plots.
33
+
34
+ Currently supported methods are:
35
+ - auto (calculates importance based on estimator's default implementation of feature importance;
36
+ estimator must be tree-based)
37
+ Note: if none provided, it uses lightgbm's LGBMRegressor as estimator, and "gain" as
38
+ importance type
39
+ - permutation (calculates importance based on mean decrease in accuracy when a feature column is permuted;
40
+ estimator can be any form)
41
+ - shapley (calculates shapley values; estimator must be tree-based)
42
+ Hint: for permutation, downsample data for better performance especially if X.shape[1] is large
43
+
44
+ Args:
45
+ method (str): auto, permutation, shapley
46
+ control_name (str/int/float): name of control group
47
+ X (np.matrix): a feature matrix
48
+ tau (np.array): a treatment effect vector (estimated/actual)
49
+ classes (dict): a mapping of treatment names to indices (used for indexing tau array)
50
+ model_tau (sklearn/lightgbm/xgboost model object): a model object
51
+ features (np.array): list/array of feature names. If None, an enumerated list will be used.
52
+ normalize (bool): normalize by sum of importances if method=auto (defaults to True)
53
+ test_size (float/int): if float, represents the proportion of the dataset to include in the test split.
54
+ If int, represents the absolute number of test samples (used for estimating
55
+ permutation importance)
56
+ random_state (int/RandomState instance/None): random state used in permutation importance estimation
57
+ override_checks (bool): overrides self.check_conditions (e.g. if importance/shapley values are pre-computed)
58
+ r_learners (dict): a mapping of treatment group to fitted R Learners
59
+ """
60
+ self.method = method
61
+ self.control_name = control_name
62
+ self.X = convert_pd_to_np(X)
63
+ self.tau = convert_pd_to_np(tau)
64
+ if self.tau is not None and self.tau.ndim == 1:
65
+ self.tau = self.tau.reshape(-1, 1)
66
+ self.classes = classes
67
+ self.model_tau = (
68
+ LGBMRegressor(importance_type="gain") if model_tau is None else model_tau
69
+ )
70
+ self.features = features
71
+ self.normalize = normalize
72
+ self.test_size = test_size
73
+ self.random_state = random_state
74
+ self.override_checks = override_checks
75
+ self.r_learners = r_learners
76
+
77
+ if not self.override_checks:
78
+ self.check_conditions()
79
+ self.create_feature_names()
80
+ self.build_new_tau_models()
81
+
82
+ def check_conditions(self):
83
+ """
84
+ Checks for multiple conditions:
85
+ - method is valid
86
+ - X, tau, and classes are specified
87
+ - model_tau has feature_importances_ attribute after fitting
88
+ """
89
+ assert self.method in VALID_METHODS, "Current supported methods: {}".format(
90
+ ", ".join(VALID_METHODS)
91
+ )
92
+
93
+ assert all(
94
+ obj is not None for obj in (self.X, self.tau, self.classes)
95
+ ), "X, tau, and classes must be provided."
96
+
97
+ model_test = deepcopy(self.model_tau)
98
+ model_test.fit(
99
+ [[0], [1]], [0, 1]
100
+ ) # Fit w/ dummy data to check for feature_importances_ below
101
+ assert hasattr(
102
+ model_test, "feature_importances_"
103
+ ), "model_tau must have the feature_importances_ method (after fitting)"
104
+
105
+ def create_feature_names(self):
106
+ """
107
+ Creates feature names (simple enumerated list) if not provided in __init__.
108
+ """
109
+ if self.features is None:
110
+ num_features = self.X.shape[1]
111
+ self.features = ["Feature_{:03d}".format(i) for i in range(num_features)]
112
+
113
+ def build_new_tau_models(self):
114
+ """
115
+ Builds tau models (using X to predict estimated/actual tau) for each treatment group.
116
+ """
117
+ if self.method in ("permutation"):
118
+ self.X_train, self.X_test, self.tau_train, self.tau_test = train_test_split(
119
+ self.X,
120
+ self.tau,
121
+ test_size=self.test_size,
122
+ random_state=self.random_state,
123
+ )
124
+ else:
125
+ self.X_train, self.tau_train = self.X, self.tau
126
+
127
+ if self.r_learners is not None:
128
+ self.models_tau = deepcopy(self.r_learners)
129
+ else:
130
+ self.models_tau = {
131
+ group: deepcopy(self.model_tau) for group in self.classes
132
+ }
133
+ for group, idx in self.classes.items():
134
+ self.models_tau[group].fit(self.X_train, self.tau_train[:, idx])
135
+
136
+ def get_importance(self):
137
+ """
138
+ Calculates feature importances for each treatment group, based on specified method in __init__.
139
+ """
140
+ importance_catalog = {
141
+ "auto": self.default_importance,
142
+ "permutation": self.perm_importance,
143
+ }
144
+ importance_dict = importance_catalog[self.method]()
145
+
146
+ importance_dict = {
147
+ group: pd.Series(array, index=self.features).sort_values(ascending=False)
148
+ for group, array in importance_dict.items()
149
+ }
150
+ return importance_dict
151
+
152
+ def default_importance(self):
153
+ """
154
+ Calculates feature importances for each treatment group, based on the model_tau's default implementation.
155
+ """
156
+ importance_dict = {}
157
+ if self.r_learners is not None:
158
+ self.models_tau = deepcopy(self.r_learners)
159
+ for group, idx in self.classes.items():
160
+ importance_dict[group] = self.models_tau[group].feature_importances_
161
+ if self.normalize:
162
+ importance_dict[group] = (
163
+ importance_dict[group] / importance_dict[group].sum()
164
+ )
165
+
166
+ return importance_dict
167
+
168
+ def perm_importance(self):
169
+ """
170
+ Calculates feature importances for each treatment group, based on the permutation method.
171
+ """
172
+ importance_dict = {}
173
+ if self.r_learners is not None:
174
+ self.models_tau = deepcopy(self.r_learners)
175
+ self.X_test, self.tau_test = self.X, self.tau
176
+ for group, idx in self.classes.items():
177
+ perm_estimator = self.models_tau[group]
178
+ importance_dict[group] = permutation_importance(
179
+ estimator=perm_estimator,
180
+ X=self.X_test,
181
+ y=self.tau_test[:, idx],
182
+ random_state=self.random_state,
183
+ ).importances_mean
184
+
185
+ return importance_dict
186
+
187
+ def get_shap_values(self):
188
+ """
189
+ Calculates shapley values for each treatment group.
190
+ """
191
+ shap_dict = {}
192
+ for group, mod in self.models_tau.items():
193
+ explainer = shap.TreeExplainer(mod)
194
+ if self.r_learners is not None:
195
+ explainer.model.original_model.params["objective"] = (
196
+ None # hacky way of running shap without error
197
+ )
198
+ shap_values = explainer.shap_values(self.X)
199
+ shap_dict[group] = shap_values
200
+
201
+ return shap_dict
202
+
203
+ def plot_importance(self, importance_dict=None, title_prefix="", figsize=(12, 8)):
204
+ """
205
+ Calculates and plots feature importances for each treatment group, based on specified method in __init__.
206
+ Skips the calculation part if importance_dict is given.
207
+ Args:
208
+ importance_dict (optional, dict): a dict of feature importance matrics. If None, importance_dict will be
209
+ computed.
210
+ title_prefix (optional, str): a prefix to the title of the plot.
211
+ figsize (optional, tuple): the size of the figure.
212
+ """
213
+ if importance_dict is None:
214
+ importance_dict = self.get_importance()
215
+ for group, series in importance_dict.items():
216
+ plt.figure()
217
+ series.sort_values().plot(kind="barh", figsize=figsize)
218
+ title = group
219
+ if title_prefix != "":
220
+ title = "{} - {}".format(title_prefix, title)
221
+ plt.title(title)
222
+
223
+ def plot_shap_values(self, shap_dict=None, **kwargs):
224
+ """
225
+ Calculates and plots the distribution of shapley values of each feature, for each treatment group.
226
+ Skips the calculation part if shap_dict is given.
227
+
228
+ Args:
229
+ shap_dict (optional, dict): a dict of shapley value matrics. If None, shap_dict will be computed.
230
+ """
231
+ if shap_dict is None:
232
+ shap_dict = self.get_shap_values()
233
+
234
+ for group, values in shap_dict.items():
235
+ plt.title(group)
236
+ shap.summary_plot(
237
+ values, features=self.X, feature_names=self.features, **kwargs
238
+ )
239
+
240
+ def plot_shap_dependence(
241
+ self,
242
+ treatment_group,
243
+ feature_idx,
244
+ shap_dict=None,
245
+ interaction_idx="auto",
246
+ **kwargs,
247
+ ):
248
+ """
249
+ Plots dependency of shapley values for a specified feature, colored by an interaction feature.
250
+ Skips the calculation part if shap_dict is given.
251
+
252
+ This plots the value of the feature on the x-axis and the SHAP value of the same feature
253
+ on the y-axis. This shows how the model depends on the given feature, and is like a
254
+ richer extension of the classical partial dependence plots. Vertical dispersion of the
255
+ data points represents interaction effects.
256
+
257
+ Args:
258
+ treatment_group (str or int): name of treatment group to create dependency plot on
259
+ feature_idx (str or int): feature index/name to create dependency plot on
260
+ shap_dict (optional, dict): a dict of shapley value matrices. If None, shap_dict will be computed.
261
+ interaction_idx (optional, str or int): feature index/name used in coloring scheme as interaction feature.
262
+ If "auto" then shap.common.approximate_interactions is used to pick what seems to be the
263
+ strongest interaction (note that to find to true strongest interaction you need to compute
264
+ the SHAP interaction values).
265
+ """
266
+ if shap_dict is None:
267
+ shap_dict = self.get_shap_values()
268
+
269
+ shap_values = shap_dict[treatment_group]
270
+
271
+ shap.dependence_plot(
272
+ feature_idx,
273
+ shap_values,
274
+ self.X,
275
+ interaction_index=interaction_idx,
276
+ feature_names=self.features,
277
+ **kwargs,
278
+ )
causalml/source/causalml/inference/meta/rlearner.py ADDED
@@ -0,0 +1,695 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from copy import deepcopy
2
+ import logging
3
+ import numpy as np
4
+ from tqdm import tqdm
5
+ from scipy.stats import norm
6
+ from sklearn.model_selection import cross_val_predict, KFold, train_test_split
7
+ from xgboost import XGBRegressor
8
+
9
+ from causalml.inference.meta.base import BaseLearner
10
+ from causalml.inference.meta.utils import (
11
+ check_treatment_vector,
12
+ get_xgboost_objective_metric,
13
+ convert_pd_to_np,
14
+ get_weighted_variance,
15
+ )
16
+ from causalml.propensity import ElasticNetPropensityModel
17
+
18
+ logger = logging.getLogger("causalml")
19
+
20
+
21
+ class BaseRLearner(BaseLearner):
22
+ """A parent class for R-learner classes.
23
+
24
+ An R-learner estimates treatment effects with two machine learning models and the propensity score.
25
+
26
+ Details of R-learner are available at `Nie and Wager (2019) <https://arxiv.org/abs/1712.04912>`_.
27
+ """
28
+
29
+ def __init__(
30
+ self,
31
+ learner=None,
32
+ outcome_learner=None,
33
+ effect_learner=None,
34
+ propensity_learner=ElasticNetPropensityModel(),
35
+ ate_alpha=0.05,
36
+ control_name=0,
37
+ n_fold=5,
38
+ random_state=None,
39
+ cv_n_jobs=-1,
40
+ ):
41
+ """Initialize an R-learner.
42
+
43
+ Args:
44
+ learner (optional): a model to estimate outcomes and treatment effects
45
+ outcome_learner (optional): a model to estimate outcomes
46
+ effect_learner (optional): a model to estimate treatment effects. It needs to take `sample_weight` as an
47
+ input argument for `fit()`
48
+ propensity_learner (optional): a model to estimate propensity scores. `ElasticNetPropensityModel()` will
49
+ be used by default.
50
+ ate_alpha (float, optional): the confidence level alpha of the ATE estimate
51
+ control_name (str or int, optional): name of control group
52
+ n_fold (int, optional): the number of cross validation folds for outcome_learner
53
+ random_state (int or RandomState, optional): a seed (int) or random number generator (RandomState)
54
+ cv_n_jobs (int, optional): number of parallel jobs to run for cross_val_predict. -1 means using all
55
+ processors
56
+ """
57
+ assert (learner is not None) or (
58
+ (outcome_learner is not None) and (effect_learner is not None)
59
+ )
60
+ assert propensity_learner is not None
61
+
62
+ self.model_mu = (
63
+ outcome_learner if outcome_learner is not None else deepcopy(learner)
64
+ )
65
+ self.model_tau = (
66
+ effect_learner if effect_learner is not None else deepcopy(learner)
67
+ )
68
+ self.model_p = propensity_learner
69
+
70
+ self.ate_alpha = ate_alpha
71
+ self.control_name = control_name
72
+
73
+ self.random_state = random_state
74
+ self.cv = KFold(n_splits=n_fold, shuffle=True, random_state=random_state)
75
+ self.cv_n_jobs = cv_n_jobs
76
+
77
+ self.propensity = None
78
+ self.propensity_model = None
79
+
80
+ def __repr__(self):
81
+ return (
82
+ f"{self.__class__.__name__}\n"
83
+ f"\toutcome_learner={self.model_mu.__repr__()}\n"
84
+ f"\teffect_learner={self.model_tau.__repr__()}\n"
85
+ f"\tpropensity_learner={self.model_p.__repr__()}"
86
+ )
87
+
88
+ def fit(self, X, treatment, y, p=None, sample_weight=None, verbose=True):
89
+ """Fit the treatment effect and outcome models of the R learner.
90
+
91
+ Args:
92
+ X (np.matrix or np.array or pd.Dataframe): a feature matrix
93
+ treatment (np.array or pd.Series): a treatment vector
94
+ y (np.array or pd.Series): an outcome vector
95
+ p (np.ndarray or pd.Series or dict, optional): an array of propensity scores of float (0,1) in the
96
+ single-treatment case; or, a dictionary of treatment groups that map to propensity vectors of
97
+ float (0,1); if None will run ElasticNetPropensityModel() to generate the propensity scores.
98
+ sample_weight (np.array or pd.Series, optional): an array of sample weights indicating the
99
+ weight of each observation for `effect_learner`. If None, it assumes equal weight.
100
+ verbose (bool, optional): whether to output progress logs
101
+ """
102
+ X, treatment, y = convert_pd_to_np(X, treatment, y)
103
+ check_treatment_vector(treatment, self.control_name)
104
+ if sample_weight is not None:
105
+ assert len(sample_weight) == len(
106
+ y
107
+ ), "Data length must be equal for sample_weight and the input data"
108
+ sample_weight = convert_pd_to_np(sample_weight)
109
+ self.t_groups = np.unique(treatment[treatment != self.control_name])
110
+ self.t_groups.sort()
111
+
112
+ if p is None:
113
+ self._set_propensity_models(X=X, treatment=treatment, y=y)
114
+ p = self.propensity
115
+ else:
116
+ p = self._format_p(p, self.t_groups)
117
+
118
+ self._classes = {group: i for i, group in enumerate(self.t_groups)}
119
+ self.models_tau = {group: deepcopy(self.model_tau) for group in self.t_groups}
120
+ self.vars_c = {}
121
+ self.vars_t = {}
122
+
123
+ if verbose:
124
+ logger.info("generating out-of-fold CV outcome estimates")
125
+ yhat = cross_val_predict(self.model_mu, X, y, cv=self.cv, n_jobs=self.cv_n_jobs)
126
+
127
+ for group in self.t_groups:
128
+ mask = (treatment == group) | (treatment == self.control_name)
129
+ treatment_filt = treatment[mask]
130
+ X_filt = X[mask]
131
+ y_filt = y[mask]
132
+ yhat_filt = yhat[mask]
133
+ p_filt = p[group][mask]
134
+ w = (treatment_filt == group).astype(int)
135
+
136
+ weight = (w - p_filt) ** 2
137
+ diff_c = y_filt[w == 0] - yhat_filt[w == 0]
138
+ diff_t = y_filt[w == 1] - yhat_filt[w == 1]
139
+ if sample_weight is not None:
140
+ sample_weight_filt = sample_weight[mask]
141
+ sample_weight_filt_c = sample_weight_filt[w == 0]
142
+ sample_weight_filt_t = sample_weight_filt[w == 1]
143
+ self.vars_c[group] = get_weighted_variance(diff_c, sample_weight_filt_c)
144
+ self.vars_t[group] = get_weighted_variance(diff_t, sample_weight_filt_t)
145
+ weight *= sample_weight_filt # update weight
146
+ else:
147
+ self.vars_c[group] = diff_c.var()
148
+ self.vars_t[group] = diff_t.var()
149
+
150
+ if verbose:
151
+ logger.info(
152
+ "training the treatment effect model for {} with R-loss".format(
153
+ group
154
+ )
155
+ )
156
+ self.models_tau[group].fit(
157
+ X_filt, (y_filt - yhat_filt) / (w - p_filt), sample_weight=weight
158
+ )
159
+
160
+ def predict(self, X, p=None):
161
+ """Predict treatment effects.
162
+
163
+ Args:
164
+ X (np.matrix or np.array or pd.Dataframe): a feature matrix
165
+
166
+ Returns:
167
+ (numpy.ndarray): Predictions of treatment effects.
168
+ """
169
+ X = convert_pd_to_np(X)
170
+ te = np.zeros((X.shape[0], self.t_groups.shape[0]))
171
+ for i, group in enumerate(self.t_groups):
172
+ dhat = self.models_tau[group].predict(X)
173
+ te[:, i] = dhat
174
+
175
+ return te
176
+
177
+ def fit_predict(
178
+ self,
179
+ X,
180
+ treatment,
181
+ y,
182
+ p=None,
183
+ sample_weight=None,
184
+ return_ci=False,
185
+ n_bootstraps=1000,
186
+ bootstrap_size=10000,
187
+ verbose=True,
188
+ ):
189
+ """Fit the treatment effect and outcome models of the R learner and predict treatment effects.
190
+
191
+ Args:
192
+ X (np.matrix or np.array or pd.Dataframe): a feature matrix
193
+ treatment (np.array or pd.Series): a treatment vector
194
+ y (np.array or pd.Series): an outcome vector
195
+ p (np.ndarray or pd.Series or dict, optional): an array of propensity scores of float (0,1) in the
196
+ single-treatment case; or, a dictionary of treatment groups that map to propensity vectors of
197
+ float (0,1); if None will run ElasticNetPropensityModel() to generate the propensity scores.
198
+ sample_weight (np.array or pd.Series, optional): an array of sample weights indicating the
199
+ weight of each observation for `effect_learner`. If None, it assumes equal weight.
200
+ return_ci (bool): whether to return confidence intervals
201
+ n_bootstraps (int): number of bootstrap iterations
202
+ bootstrap_size (int): number of samples per bootstrap
203
+ verbose (bool): whether to output progress logs
204
+ Returns:
205
+ (numpy.ndarray): Predictions of treatment effects. Output dim: [n_samples, n_treatment].
206
+ If return_ci, returns CATE [n_samples, n_treatment], LB [n_samples, n_treatment],
207
+ UB [n_samples, n_treatment]
208
+ """
209
+ X, treatment, y = convert_pd_to_np(X, treatment, y)
210
+ self.fit(X, treatment, y, p, sample_weight, verbose=verbose)
211
+ te = self.predict(X)
212
+
213
+ if not return_ci:
214
+ return te
215
+ else:
216
+ t_groups_global = self.t_groups
217
+ _classes_global = self._classes
218
+ model_mu_global = deepcopy(self.model_mu)
219
+ models_tau_global = deepcopy(self.models_tau)
220
+ te_bootstraps = np.zeros(
221
+ shape=(X.shape[0], self.t_groups.shape[0], n_bootstraps)
222
+ )
223
+
224
+ logger.info("Bootstrap Confidence Intervals")
225
+ for i in tqdm(range(n_bootstraps)):
226
+ if p is None:
227
+ p = self.propensity
228
+ else:
229
+ p = self._format_p(p, self.t_groups)
230
+ te_b = self.bootstrap(X, treatment, y, p, size=bootstrap_size)
231
+ te_bootstraps[:, :, i] = te_b
232
+
233
+ te_lower = np.percentile(te_bootstraps, (self.ate_alpha / 2) * 100, axis=2)
234
+ te_upper = np.percentile(
235
+ te_bootstraps, (1 - self.ate_alpha / 2) * 100, axis=2
236
+ )
237
+
238
+ # set member variables back to global (currently last bootstrapped outcome)
239
+ self.t_groups = t_groups_global
240
+ self._classes = _classes_global
241
+ self.model_mu = deepcopy(model_mu_global)
242
+ self.models_tau = deepcopy(models_tau_global)
243
+
244
+ return (te, te_lower, te_upper)
245
+
246
+ def estimate_ate(
247
+ self,
248
+ X,
249
+ treatment=None,
250
+ y=None,
251
+ p=None,
252
+ sample_weight=None,
253
+ bootstrap_ci=False,
254
+ n_bootstraps=1000,
255
+ bootstrap_size=10000,
256
+ pretrain=False,
257
+ ):
258
+ """Estimate the Average Treatment Effect (ATE).
259
+
260
+ Args:
261
+ X (np.matrix or np.array or pd.Dataframe): a feature matrix
262
+ treatment (np.array or pd.Series): only needed when pretrain=False, a treatment vector
263
+ y (np.array or pd.Series):only needed when pretrain=False, an outcome vector
264
+ p (np.ndarray or pd.Series or dict, optional): an array of propensity scores of float (0,1) in the
265
+ single-treatment case; or, a dictionary of treatment groups that map to propensity vectors of
266
+ float (0,1); if None will run ElasticNetPropensityModel() to generate the propensity scores.
267
+ sample_weight (np.array or pd.Series, optional): an array of sample weights indicating the
268
+ weight of each observation for `effect_learner`. If None, it assumes equal weight.
269
+ bootstrap_ci (bool): whether run bootstrap for confidence intervals
270
+ n_bootstraps (int): number of bootstrap iterations
271
+ bootstrap_size (int): number of samples per bootstrap
272
+ pretrain (bool): whether a model has been fit, default False.
273
+ Returns:
274
+ The mean and confidence interval (LB, UB) of the ATE estimate.
275
+ """
276
+ X, treatment, y = convert_pd_to_np(X, treatment, y)
277
+ if pretrain:
278
+ te = self.predict(X, p)
279
+ else:
280
+ if not len(treatment) or not len(y):
281
+ raise ValueError("treatmeng and y must be provided when pretrain=False")
282
+ te = self.fit_predict(X, treatment, y, p, sample_weight, return_ci=False)
283
+
284
+ ate = np.zeros(self.t_groups.shape[0])
285
+ ate_lb = np.zeros(self.t_groups.shape[0])
286
+ ate_ub = np.zeros(self.t_groups.shape[0])
287
+
288
+ for i, group in enumerate(self.t_groups):
289
+ w = (treatment == group).astype(int)
290
+ prob_treatment = float(sum(w)) / X.shape[0]
291
+ _ate = te[:, i].mean()
292
+
293
+ se = (
294
+ np.sqrt(
295
+ (self.vars_t[group] / prob_treatment)
296
+ + (self.vars_c[group] / (1 - prob_treatment))
297
+ + te[:, i].var()
298
+ )
299
+ / X.shape[0]
300
+ )
301
+
302
+ _ate_lb = _ate - se * norm.ppf(1 - self.ate_alpha / 2)
303
+ _ate_ub = _ate + se * norm.ppf(1 - self.ate_alpha / 2)
304
+
305
+ ate[i] = _ate
306
+ ate_lb[i] = _ate_lb
307
+ ate_ub[i] = _ate_ub
308
+
309
+ if not bootstrap_ci:
310
+ return ate, ate_lb, ate_ub
311
+ else:
312
+ t_groups_global = self.t_groups
313
+ _classes_global = self._classes
314
+ model_mu_global = deepcopy(self.model_mu)
315
+ models_tau_global = deepcopy(self.models_tau)
316
+
317
+ logger.info("Bootstrap Confidence Intervals for ATE")
318
+ ate_bootstraps = np.zeros(shape=(self.t_groups.shape[0], n_bootstraps))
319
+
320
+ for n in tqdm(range(n_bootstraps)):
321
+ if p is None:
322
+ p = self.propensity
323
+ else:
324
+ p = self._format_p(p, self.t_groups)
325
+ cate_b = self.bootstrap(X, treatment, y, p, size=bootstrap_size)
326
+ ate_bootstraps[:, n] = cate_b.mean(axis=0)
327
+
328
+ ate_lower = np.percentile(
329
+ ate_bootstraps, (self.ate_alpha / 2) * 100, axis=1
330
+ )
331
+ ate_upper = np.percentile(
332
+ ate_bootstraps, (1 - self.ate_alpha / 2) * 100, axis=1
333
+ )
334
+
335
+ # set member variables back to global (currently last bootstrapped outcome)
336
+ self.t_groups = t_groups_global
337
+ self._classes = _classes_global
338
+ self.model_mu = deepcopy(model_mu_global)
339
+ self.models_tau = deepcopy(models_tau_global)
340
+ return ate, ate_lower, ate_upper
341
+
342
+
343
+ class BaseRRegressor(BaseRLearner):
344
+ """
345
+ A parent class for R-learner regressor classes.
346
+ """
347
+
348
+ def __init__(
349
+ self,
350
+ learner=None,
351
+ outcome_learner=None,
352
+ effect_learner=None,
353
+ propensity_learner=ElasticNetPropensityModel(),
354
+ ate_alpha=0.05,
355
+ control_name=0,
356
+ n_fold=5,
357
+ random_state=None,
358
+ ):
359
+ """Initialize an R-learner regressor.
360
+
361
+ Args:
362
+ learner (optional): a model to estimate outcomes and treatment effects
363
+ outcome_learner (optional): a model to estimate outcomes
364
+ effect_learner (optional): a model to estimate treatment effects. It needs to take `sample_weight` as an
365
+ input argument for `fit()`
366
+ propensity_learner (optional): a model to estimate propensity scores. `ElasticNetPropensityModel()` will
367
+ be used by default.
368
+ ate_alpha (float, optional): the confidence level alpha of the ATE estimate
369
+ control_name (str or int, optional): name of control group
370
+ n_fold (int, optional): the number of cross validation folds for outcome_learner
371
+ random_state (int or RandomState, optional): a seed (int) or random number generator (RandomState)
372
+ """
373
+ super().__init__(
374
+ learner=learner,
375
+ outcome_learner=outcome_learner,
376
+ effect_learner=effect_learner,
377
+ propensity_learner=propensity_learner,
378
+ ate_alpha=ate_alpha,
379
+ control_name=control_name,
380
+ n_fold=n_fold,
381
+ random_state=random_state,
382
+ )
383
+
384
+
385
+ class BaseRClassifier(BaseRLearner):
386
+ """
387
+ A parent class for R-learner classifier classes.
388
+ """
389
+
390
+ def __init__(
391
+ self,
392
+ outcome_learner=None,
393
+ effect_learner=None,
394
+ propensity_learner=ElasticNetPropensityModel(),
395
+ ate_alpha=0.05,
396
+ control_name=0,
397
+ n_fold=5,
398
+ random_state=None,
399
+ ):
400
+ """Initialize an R-learner classifier.
401
+
402
+ Args:
403
+ outcome_learner: a model to estimate outcomes. Should be a classifier.
404
+ effect_learner: a model to estimate treatment effects. It needs to take `sample_weight` as an
405
+ input argument for `fit()`. Should be a regressor.
406
+ propensity_learner (optional): a model to estimate propensity scores. `ElasticNetPropensityModel()` will
407
+ be used by default.
408
+ ate_alpha (float, optional): the confidence level alpha of the ATE estimate
409
+ control_name (str or int, optional): name of control group
410
+ n_fold (int, optional): the number of cross validation folds for outcome_learner
411
+ random_state (int or RandomState, optional): a seed (int) or random number generator (RandomState)
412
+ """
413
+ super().__init__(
414
+ learner=None,
415
+ outcome_learner=outcome_learner,
416
+ effect_learner=effect_learner,
417
+ propensity_learner=propensity_learner,
418
+ ate_alpha=ate_alpha,
419
+ control_name=control_name,
420
+ n_fold=n_fold,
421
+ random_state=random_state,
422
+ )
423
+
424
+ if (outcome_learner is None) and (effect_learner is None):
425
+ raise ValueError(
426
+ "Either the outcome learner or the effect learner must be specified."
427
+ )
428
+
429
+ def fit(self, X, treatment, y, p=None, sample_weight=None, verbose=True):
430
+ """Fit the treatment effect and outcome models of the R learner.
431
+
432
+ Args:
433
+ X (np.matrix or np.array or pd.Dataframe): a feature matrix
434
+ treatment (np.array or pd.Series): a treatment vector
435
+ y (np.array or pd.Series): an outcome vector
436
+ p (np.ndarray or pd.Series or dict, optional): an array of propensity scores of float (0,1) in the
437
+ single-treatment case; or, a dictionary of treatment groups that map to propensity vectors of
438
+ float (0,1); if None will run ElasticNetPropensityModel() to generate the propensity scores.
439
+ sample_weight (np.array or pd.Series, optional): an array of sample weights indicating the
440
+ weight of each observation for `effect_learner`. If None, it assumes equal weight.
441
+ verbose (bool, optional): whether to output progress logs
442
+ """
443
+ X, treatment, y = convert_pd_to_np(X, treatment, y)
444
+ check_treatment_vector(treatment, self.control_name)
445
+ if sample_weight is not None:
446
+ assert len(sample_weight) == len(
447
+ y
448
+ ), "Data length must be equal for sample_weight and the input data"
449
+ sample_weight = convert_pd_to_np(sample_weight)
450
+ self.t_groups = np.unique(treatment[treatment != self.control_name])
451
+ self.t_groups.sort()
452
+
453
+ if p is None:
454
+ self._set_propensity_models(X=X, treatment=treatment, y=y)
455
+ p = self.propensity
456
+ else:
457
+ p = self._format_p(p, self.t_groups)
458
+
459
+ self._classes = {group: i for i, group in enumerate(self.t_groups)}
460
+ self.models_tau = {group: deepcopy(self.model_tau) for group in self.t_groups}
461
+ self.vars_c = {}
462
+ self.vars_t = {}
463
+
464
+ if verbose:
465
+ logger.info("generating out-of-fold CV outcome estimates")
466
+ yhat = cross_val_predict(
467
+ self.model_mu, X, y, cv=self.cv, method="predict_proba", n_jobs=-1
468
+ )[:, 1]
469
+
470
+ for group in self.t_groups:
471
+ mask = (treatment == group) | (treatment == self.control_name)
472
+ treatment_filt = treatment[mask]
473
+ X_filt = X[mask]
474
+ y_filt = y[mask]
475
+ yhat_filt = yhat[mask]
476
+ p_filt = p[group][mask]
477
+ w = (treatment_filt == group).astype(int)
478
+
479
+ weight = (w - p_filt) ** 2
480
+ diff_c = y_filt[w == 0] - yhat_filt[w == 0]
481
+ diff_t = y_filt[w == 1] - yhat_filt[w == 1]
482
+ if sample_weight is not None:
483
+ sample_weight_filt = sample_weight[mask]
484
+ sample_weight_filt_c = sample_weight_filt[w == 0]
485
+ sample_weight_filt_t = sample_weight_filt[w == 1]
486
+ self.vars_c[group] = get_weighted_variance(diff_c, sample_weight_filt_c)
487
+ self.vars_t[group] = get_weighted_variance(diff_t, sample_weight_filt_t)
488
+ weight *= sample_weight_filt # update weight
489
+ else:
490
+ self.vars_c[group] = diff_c.var()
491
+ self.vars_t[group] = diff_t.var()
492
+
493
+ if verbose:
494
+ logger.info(
495
+ "training the treatment effect model for {} with R-loss".format(
496
+ group
497
+ )
498
+ )
499
+ self.models_tau[group].fit(
500
+ X_filt, (y_filt - yhat_filt) / (w - p_filt), sample_weight=weight
501
+ )
502
+
503
+ def predict(self, X, p=None):
504
+ """Predict treatment effects.
505
+
506
+ Args:
507
+ X (np.matrix or np.array or pd.Dataframe): a feature matrix
508
+
509
+ Returns:
510
+ (numpy.ndarray): Predictions of treatment effects.
511
+ """
512
+ X = convert_pd_to_np(X)
513
+ te = np.zeros((X.shape[0], self.t_groups.shape[0]))
514
+ for i, group in enumerate(self.t_groups):
515
+ dhat = self.models_tau[group].predict(X)
516
+ te[:, i] = dhat
517
+
518
+ return te
519
+
520
+
521
+ class XGBRRegressor(BaseRRegressor):
522
+ def __init__(
523
+ self,
524
+ early_stopping=True,
525
+ test_size=0.3,
526
+ early_stopping_rounds=30,
527
+ effect_learner_objective="reg:squarederror",
528
+ effect_learner_n_estimators=500,
529
+ random_state=42,
530
+ *args,
531
+ **kwargs,
532
+ ):
533
+ """Initialize an R-learner regressor with XGBoost model using pairwise ranking objective.
534
+
535
+ Args:
536
+ early_stopping: whether or not to use early stopping when fitting effect learner
537
+ test_size (float, optional): the proportion of the dataset to use as validation set when early stopping is
538
+ enabled
539
+ early_stopping_rounds (int, optional): validation metric needs to improve at least once in every
540
+ early_stopping_rounds round(s) to continue training
541
+ effect_learner_objective (str, optional): the learning objective for the effect learner
542
+ (default = 'reg:squarederror')
543
+ effect_learner_n_estimators (int, optional): number of trees to fit for the effect learner (default = 500)
544
+ """
545
+
546
+ assert isinstance(random_state, int), "random_state should be int."
547
+
548
+ objective, metric = get_xgboost_objective_metric(effect_learner_objective)
549
+ self.effect_learner_objective = objective
550
+ self.effect_learner_eval_metric = metric
551
+ self.effect_learner_n_estimators = effect_learner_n_estimators
552
+ self.early_stopping = early_stopping
553
+ if self.early_stopping:
554
+ self.test_size = test_size
555
+ self.early_stopping_rounds = early_stopping_rounds
556
+
557
+ effect_learner = XGBRegressor(
558
+ objective=self.effect_learner_objective,
559
+ n_estimators=self.effect_learner_n_estimators,
560
+ eval_metric=self.effect_learner_eval_metric,
561
+ early_stopping_rounds=self.early_stopping_rounds,
562
+ random_state=random_state,
563
+ *args,
564
+ **kwargs,
565
+ )
566
+ else:
567
+ effect_learner = XGBRegressor(
568
+ objective=self.effect_learner_objective,
569
+ n_estimators=self.effect_learner_n_estimators,
570
+ eval_metric=self.effect_learner_eval_metric,
571
+ random_state=random_state,
572
+ *args,
573
+ **kwargs,
574
+ )
575
+
576
+ super().__init__(
577
+ outcome_learner=XGBRegressor(random_state=random_state, *args, **kwargs),
578
+ effect_learner=effect_learner,
579
+ )
580
+
581
+ def fit(self, X, treatment, y, p=None, sample_weight=None, verbose=True):
582
+ """Fit the treatment effect and outcome models of the R learner.
583
+
584
+ Args:
585
+ X (np.matrix or np.array or pd.Dataframe): a feature matrix
586
+ y (np.array or pd.Series): an outcome vector
587
+ p (np.ndarray or pd.Series or dict, optional): an array of propensity scores of float (0,1) in the
588
+ single-treatment case; or, a dictionary of treatment groups that map to propensity vectors of
589
+ float (0,1); if None will run ElasticNetPropensityModel() to generate the propensity scores.
590
+ sample_weight (np.array or pd.Series, optional): an array of sample weights indicating the
591
+ weight of each observation for `effect_learner`. If None, it assumes equal weight.
592
+ verbose (bool, optional): whether to output progress logs
593
+ """
594
+ X, treatment, y = convert_pd_to_np(X, treatment, y)
595
+ check_treatment_vector(treatment, self.control_name)
596
+ # initialize equal sample weight if it's not provided, for simplicity purpose
597
+ sample_weight = (
598
+ convert_pd_to_np(sample_weight)
599
+ if sample_weight is not None
600
+ else convert_pd_to_np(np.ones(len(y)))
601
+ )
602
+ assert len(sample_weight) == len(
603
+ y
604
+ ), "Data length must be equal for sample_weight and the input data"
605
+ self.t_groups = np.unique(treatment[treatment != self.control_name])
606
+ self.t_groups.sort()
607
+
608
+ if p is None:
609
+ self._set_propensity_models(X=X, treatment=treatment, y=y)
610
+ p = self.propensity
611
+ else:
612
+ p = self._format_p(p, self.t_groups)
613
+
614
+ self._classes = {group: i for i, group in enumerate(self.t_groups)}
615
+ self.models_tau = {group: deepcopy(self.model_tau) for group in self.t_groups}
616
+ self.vars_c = {}
617
+ self.vars_t = {}
618
+
619
+ if verbose:
620
+ logger.info("generating out-of-fold CV outcome estimates")
621
+ yhat = cross_val_predict(self.model_mu, X, y, cv=self.cv, n_jobs=-1)
622
+
623
+ for group in self.t_groups:
624
+ treatment_mask = (treatment == group) | (treatment == self.control_name)
625
+ treatment_filt = treatment[treatment_mask]
626
+ w = (treatment_filt == group).astype(int)
627
+
628
+ X_filt = X[treatment_mask]
629
+ y_filt = y[treatment_mask]
630
+ yhat_filt = yhat[treatment_mask]
631
+ p_filt = p[group][treatment_mask]
632
+ sample_weight_filt = sample_weight[treatment_mask]
633
+
634
+ if verbose:
635
+ logger.info(
636
+ "training the treatment effect model for {} with R-loss".format(
637
+ group
638
+ )
639
+ )
640
+
641
+ if self.early_stopping:
642
+ (
643
+ X_train_filt,
644
+ X_test_filt,
645
+ y_train_filt,
646
+ y_test_filt,
647
+ yhat_train_filt,
648
+ yhat_test_filt,
649
+ w_train,
650
+ w_test,
651
+ p_train_filt,
652
+ p_test_filt,
653
+ sample_weight_train_filt,
654
+ sample_weight_test_filt,
655
+ ) = train_test_split(
656
+ X_filt,
657
+ y_filt,
658
+ yhat_filt,
659
+ w,
660
+ p_filt,
661
+ sample_weight_filt,
662
+ test_size=self.test_size,
663
+ random_state=self.random_state,
664
+ )
665
+
666
+ self.models_tau[group].fit(
667
+ X=X_train_filt,
668
+ y=(y_train_filt - yhat_train_filt) / (w_train - p_train_filt),
669
+ sample_weight=sample_weight_train_filt
670
+ * ((w_train - p_train_filt) ** 2),
671
+ eval_set=[
672
+ (
673
+ X_test_filt,
674
+ (y_test_filt - yhat_test_filt) / (w_test - p_test_filt),
675
+ )
676
+ ],
677
+ sample_weight_eval_set=[
678
+ sample_weight_test_filt * ((w_test - p_test_filt) ** 2)
679
+ ],
680
+ verbose=verbose,
681
+ )
682
+
683
+ else:
684
+ self.models_tau[group].fit(
685
+ X_filt,
686
+ (y_filt - yhat_filt) / (w - p_filt),
687
+ sample_weight=sample_weight_filt * ((w - p_filt) ** 2),
688
+ )
689
+
690
+ diff_c = y_filt[w == 0] - yhat_filt[w == 0]
691
+ diff_t = y_filt[w == 1] - yhat_filt[w == 1]
692
+ sample_weight_filt_c = sample_weight_filt[w == 0]
693
+ sample_weight_filt_t = sample_weight_filt[w == 1]
694
+ self.vars_c[group] = get_weighted_variance(diff_c, sample_weight_filt_c)
695
+ self.vars_t[group] = get_weighted_variance(diff_t, sample_weight_filt_t)
causalml/source/causalml/inference/meta/slearner.py ADDED
@@ -0,0 +1,411 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+ import numpy as np
3
+ from tqdm import tqdm
4
+ from scipy.stats import norm
5
+ from sklearn.dummy import DummyRegressor
6
+ import statsmodels.api as sm
7
+ from copy import deepcopy
8
+
9
+ from causalml.inference.meta.base import BaseLearner
10
+ from causalml.inference.meta.utils import check_treatment_vector, convert_pd_to_np
11
+ from causalml.metrics import regression_metrics, classification_metrics
12
+
13
+ logger = logging.getLogger("causalml")
14
+
15
+
16
+ class StatsmodelsOLS:
17
+ """A sklearn style wrapper class for statsmodels' OLS."""
18
+
19
+ def __init__(self, cov_type="HC1", alpha=0.05):
20
+ """Initialize a statsmodels' OLS wrapper class object.
21
+ Args:
22
+ cov_type (str, optional): covariance estimator type.
23
+ alpha (float, optional): the confidence level alpha.
24
+ """
25
+ self.cov_type = cov_type
26
+ self.alpha = alpha
27
+
28
+ def fit(self, X, y):
29
+ """Fit OLS.
30
+ Args:
31
+ X (np.matrix): a feature matrix
32
+ y (np.array): a label vector
33
+ """
34
+ # Append ones. The first column is for the treatment indicator.
35
+ X = sm.add_constant(X, prepend=False, has_constant="add")
36
+ self.model = sm.OLS(y, X).fit(cov_type=self.cov_type)
37
+ self.coefficients = self.model.params
38
+ self.conf_ints = self.model.conf_int(alpha=self.alpha)
39
+
40
+ def predict(self, X):
41
+ # Append ones. The first column is for the treatment indicator.
42
+ X = sm.add_constant(X, prepend=False, has_constant="add")
43
+ return self.model.predict(X)
44
+
45
+
46
+ class BaseSLearner(BaseLearner):
47
+ """A parent class for S-learner classes.
48
+ An S-learner estimates treatment effects with one machine learning model.
49
+ Details of S-learner are available at `Kunzel et al. (2018) <https://arxiv.org/abs/1706.03461>`_.
50
+ """
51
+
52
+ def __init__(self, learner=None, ate_alpha=0.05, control_name=0):
53
+ """Initialize an S-learner.
54
+ Args:
55
+ learner (optional): a model to estimate the treatment effect
56
+ control_name (str or int, optional): name of control group
57
+ """
58
+ if learner is not None:
59
+ self.model = learner
60
+ else:
61
+ self.model = DummyRegressor()
62
+ self.ate_alpha = ate_alpha
63
+ self.control_name = control_name
64
+
65
+ def __repr__(self):
66
+ return "{}(model={})".format(self.__class__.__name__, self.model.__repr__())
67
+
68
+ def fit(self, X, treatment, y, p=None):
69
+ """Fit the inference model
70
+ Args:
71
+ X (np.matrix, np.array, or pd.Dataframe): a feature matrix
72
+ treatment (np.array or pd.Series): a treatment vector
73
+ y (np.array or pd.Series): an outcome vector
74
+ """
75
+ X, treatment, y = convert_pd_to_np(X, treatment, y)
76
+ check_treatment_vector(treatment, self.control_name)
77
+ self.t_groups = np.unique(treatment[treatment != self.control_name])
78
+ self.t_groups.sort()
79
+ self._classes = {group: i for i, group in enumerate(self.t_groups)}
80
+ self.models = {group: deepcopy(self.model) for group in self.t_groups}
81
+
82
+ for group in self.t_groups:
83
+ mask = (treatment == group) | (treatment == self.control_name)
84
+ treatment_filt = treatment[mask]
85
+ X_filt = X[mask]
86
+ y_filt = y[mask]
87
+
88
+ w = (treatment_filt == group).astype(int)
89
+ X_new = np.hstack((w.reshape((-1, 1)), X_filt))
90
+ self.models[group].fit(X_new, y_filt)
91
+
92
+ def predict(
93
+ self, X, treatment=None, y=None, p=None, return_components=False, verbose=True
94
+ ):
95
+ """Predict treatment effects.
96
+ Args:
97
+ X (np.matrix or np.array or pd.Dataframe): a feature matrix
98
+ treatment (np.array or pd.Series, optional): a treatment vector
99
+ y (np.array or pd.Series, optional): an outcome vector
100
+ return_components (bool, optional): whether to return outcome for treatment and control seperately
101
+ verbose (bool, optional): whether to output progress logs
102
+ Returns:
103
+ (numpy.ndarray): Predictions of treatment effects.
104
+ """
105
+ X, treatment, y = convert_pd_to_np(X, treatment, y)
106
+ yhat_cs = {}
107
+ yhat_ts = {}
108
+
109
+ for group in self.t_groups:
110
+ model = self.models[group]
111
+
112
+ # set the treatment column to zero (the control group)
113
+ X_new = np.hstack((np.zeros((X.shape[0], 1)), X))
114
+ yhat_cs[group] = model.predict(X_new)
115
+
116
+ # set the treatment column to one (the treatment group)
117
+ X_new[:, 0] = 1
118
+ yhat_ts[group] = model.predict(X_new)
119
+
120
+ if (y is not None) and (treatment is not None) and verbose:
121
+ mask = (treatment == group) | (treatment == self.control_name)
122
+ treatment_filt = treatment[mask]
123
+ w = (treatment_filt == group).astype(int)
124
+ y_filt = y[mask]
125
+
126
+ yhat = np.zeros_like(y_filt, dtype=float)
127
+ yhat[w == 0] = yhat_cs[group][mask][w == 0]
128
+ yhat[w == 1] = yhat_ts[group][mask][w == 1]
129
+
130
+ logger.info("Error metrics for group {}".format(group))
131
+ regression_metrics(y_filt, yhat, w)
132
+
133
+ te = np.zeros((X.shape[0], self.t_groups.shape[0]))
134
+ for i, group in enumerate(self.t_groups):
135
+ te[:, i] = yhat_ts[group] - yhat_cs[group]
136
+
137
+ if not return_components:
138
+ return te
139
+ else:
140
+ return te, yhat_cs, yhat_ts
141
+
142
+ def fit_predict(
143
+ self,
144
+ X,
145
+ treatment,
146
+ y,
147
+ p=None,
148
+ return_ci=False,
149
+ n_bootstraps=1000,
150
+ bootstrap_size=10000,
151
+ return_components=False,
152
+ verbose=True,
153
+ ):
154
+ """Fit the inference model of the S learner and predict treatment effects.
155
+ Args:
156
+ X (np.matrix, np.array, or pd.Dataframe): a feature matrix
157
+ treatment (np.array or pd.Series): a treatment vector
158
+ y (np.array or pd.Series): an outcome vector
159
+ return_ci (bool, optional): whether to return confidence intervals
160
+ n_bootstraps (int, optional): number of bootstrap iterations
161
+ bootstrap_size (int, optional): number of samples per bootstrap
162
+ return_components (bool, optional): whether to return outcome for treatment and control seperately
163
+ verbose (bool, optional): whether to output progress logs
164
+ Returns:
165
+ (numpy.ndarray): Predictions of treatment effects. Output dim: [n_samples, n_treatment].
166
+ If return_ci, returns CATE [n_samples, n_treatment], LB [n_samples, n_treatment],
167
+ UB [n_samples, n_treatment]
168
+ """
169
+ self.fit(X, treatment, y)
170
+ te = self.predict(X, treatment, y, return_components=return_components)
171
+
172
+ if not return_ci:
173
+ return te
174
+ else:
175
+ t_groups_global = self.t_groups
176
+ _classes_global = self._classes
177
+ models_global = deepcopy(self.models)
178
+ te_bootstraps = np.zeros(
179
+ shape=(X.shape[0], self.t_groups.shape[0], n_bootstraps)
180
+ )
181
+
182
+ logger.info("Bootstrap Confidence Intervals")
183
+ for i in tqdm(range(n_bootstraps)):
184
+ te_b = self.bootstrap(X, treatment, y, size=bootstrap_size)
185
+ te_bootstraps[:, :, i] = te_b
186
+
187
+ te_lower = np.percentile(te_bootstraps, (self.ate_alpha / 2) * 100, axis=2)
188
+ te_upper = np.percentile(
189
+ te_bootstraps, (1 - self.ate_alpha / 2) * 100, axis=2
190
+ )
191
+
192
+ # set member variables back to global (currently last bootstrapped outcome)
193
+ self.t_groups = t_groups_global
194
+ self._classes = _classes_global
195
+ self.models = deepcopy(models_global)
196
+
197
+ return (te, te_lower, te_upper)
198
+
199
+ def estimate_ate(
200
+ self,
201
+ X,
202
+ treatment,
203
+ y,
204
+ p=None,
205
+ return_ci=False,
206
+ bootstrap_ci=False,
207
+ n_bootstraps=1000,
208
+ bootstrap_size=10000,
209
+ pretrain=False,
210
+ ):
211
+ """Estimate the Average Treatment Effect (ATE).
212
+
213
+ Args:
214
+ X (np.matrix, np.array, or pd.Dataframe): a feature matrix
215
+ treatment (np.array or pd.Series): a treatment vector
216
+ y (np.array or pd.Series): an outcome vector
217
+ return_ci (bool, optional): whether to return confidence intervals
218
+ bootstrap_ci (bool): whether to return confidence intervals
219
+ n_bootstraps (int): number of bootstrap iterations
220
+ bootstrap_size (int): number of samples per bootstrap
221
+ pretrain (bool): whether a model has been fit, default False.
222
+ Returns:
223
+ The mean and confidence interval (LB, UB) of the ATE estimate.
224
+ """
225
+
226
+ X, treatment, y = convert_pd_to_np(X, treatment, y)
227
+ if pretrain:
228
+ te, yhat_cs, yhat_ts = self.predict(X, treatment, y, return_components=True)
229
+ else:
230
+ te, yhat_cs, yhat_ts = self.fit_predict(
231
+ X, treatment, y, return_components=True
232
+ )
233
+
234
+ ate = np.zeros(self.t_groups.shape[0])
235
+ ate_lb = np.zeros(self.t_groups.shape[0])
236
+ ate_ub = np.zeros(self.t_groups.shape[0])
237
+
238
+ for i, group in enumerate(self.t_groups):
239
+ _ate = te[:, i].mean()
240
+
241
+ mask = (treatment == group) | (treatment == self.control_name)
242
+ treatment_filt = treatment[mask]
243
+ y_filt = y[mask]
244
+ w = (treatment_filt == group).astype(int)
245
+ prob_treatment = float(sum(w)) / w.shape[0]
246
+
247
+ yhat_c = yhat_cs[group][mask]
248
+ yhat_t = yhat_ts[group][mask]
249
+
250
+ se = np.sqrt(
251
+ (
252
+ (y_filt[w == 0] - yhat_c[w == 0]).var() / (1 - prob_treatment)
253
+ + (y_filt[w == 1] - yhat_t[w == 1]).var() / prob_treatment
254
+ + (yhat_t - yhat_c).var()
255
+ )
256
+ / y_filt.shape[0]
257
+ )
258
+
259
+ _ate_lb = _ate - se * norm.ppf(1 - self.ate_alpha / 2)
260
+ _ate_ub = _ate + se * norm.ppf(1 - self.ate_alpha / 2)
261
+
262
+ ate[i] = _ate
263
+ ate_lb[i] = _ate_lb
264
+ ate_ub[i] = _ate_ub
265
+
266
+ if not return_ci:
267
+ return ate
268
+ elif return_ci and not bootstrap_ci:
269
+ return ate, ate_lb, ate_ub
270
+ else:
271
+ t_groups_global = self.t_groups
272
+ _classes_global = self._classes
273
+ models_global = deepcopy(self.models)
274
+
275
+ logger.info("Bootstrap Confidence Intervals for ATE")
276
+ ate_bootstraps = np.zeros(shape=(self.t_groups.shape[0], n_bootstraps))
277
+
278
+ for n in tqdm(range(n_bootstraps)):
279
+ ate_b = self.bootstrap(X, treatment, y, size=bootstrap_size)
280
+ ate_bootstraps[:, n] = ate_b.mean(axis=0)
281
+
282
+ ate_lower = np.percentile(
283
+ ate_bootstraps, (self.ate_alpha / 2) * 100, axis=1
284
+ )
285
+ ate_upper = np.percentile(
286
+ ate_bootstraps, (1 - self.ate_alpha / 2) * 100, axis=1
287
+ )
288
+
289
+ # set member variables back to global (currently last bootstrapped outcome)
290
+ self.t_groups = t_groups_global
291
+ self._classes = _classes_global
292
+ self.models = deepcopy(models_global)
293
+
294
+ return ate, ate_lower, ate_upper
295
+
296
+
297
+ class BaseSRegressor(BaseSLearner):
298
+ """
299
+ A parent class for S-learner regressor classes.
300
+ """
301
+
302
+ def __init__(self, learner=None, ate_alpha=0.05, control_name=0):
303
+ """Initialize an S-learner regressor.
304
+ Args:
305
+ learner (optional): a model to estimate the treatment effect
306
+ control_name (str or int, optional): name of control group
307
+ """
308
+ super().__init__(
309
+ learner=learner, ate_alpha=ate_alpha, control_name=control_name
310
+ )
311
+
312
+
313
+ class BaseSClassifier(BaseSLearner):
314
+ """
315
+ A parent class for S-learner classifier classes.
316
+ """
317
+
318
+ def __init__(self, learner=None, ate_alpha=0.05, control_name=0):
319
+ """Initialize an S-learner classifier.
320
+ Args:
321
+ learner (optional): a model to estimate the treatment effect.
322
+ Should have a predict_proba() method.
323
+ control_name (str or int, optional): name of control group
324
+ """
325
+ super().__init__(
326
+ learner=learner, ate_alpha=ate_alpha, control_name=control_name
327
+ )
328
+
329
+ def predict(
330
+ self, X, treatment=None, y=None, p=None, return_components=False, verbose=True
331
+ ):
332
+ """Predict treatment effects.
333
+ Args:
334
+ X (np.matrix or np.array or pd.Dataframe): a feature matrix
335
+ treatment (np.array or pd.Series, optional): a treatment vector
336
+ y (np.array or pd.Series, optional): an outcome vector
337
+ return_components (bool, optional): whether to return outcome for treatment and control seperately
338
+ verbose (bool, optional): whether to output progress logs
339
+ Returns:
340
+ (numpy.ndarray): Predictions of treatment effects.
341
+ """
342
+ X, treatment, y = convert_pd_to_np(X, treatment, y)
343
+ yhat_cs = {}
344
+ yhat_ts = {}
345
+
346
+ for group in self.t_groups:
347
+ model = self.models[group]
348
+
349
+ # set the treatment column to zero (the control group)
350
+ X_new = np.hstack((np.zeros((X.shape[0], 1)), X))
351
+ yhat_cs[group] = model.predict_proba(X_new)[:, 1]
352
+
353
+ # set the treatment column to one (the treatment group)
354
+ X_new[:, 0] = 1
355
+ yhat_ts[group] = model.predict_proba(X_new)[:, 1]
356
+
357
+ if y is not None and (treatment is not None) and verbose:
358
+ mask = (treatment == group) | (treatment == self.control_name)
359
+ treatment_filt = treatment[mask]
360
+ w = (treatment_filt == group).astype(int)
361
+ y_filt = y[mask]
362
+
363
+ yhat = np.zeros_like(y_filt, dtype=float)
364
+ yhat[w == 0] = yhat_cs[group][mask][w == 0]
365
+ yhat[w == 1] = yhat_ts[group][mask][w == 1]
366
+
367
+ logger.info("Error metrics for group {}".format(group))
368
+ classification_metrics(y_filt, yhat, w)
369
+
370
+ te = np.zeros((X.shape[0], self.t_groups.shape[0]))
371
+ for i, group in enumerate(self.t_groups):
372
+ te[:, i] = yhat_ts[group] - yhat_cs[group]
373
+
374
+ if not return_components:
375
+ return te
376
+ else:
377
+ return te, yhat_cs, yhat_ts
378
+
379
+
380
+ class LRSRegressor(BaseSRegressor):
381
+ def __init__(self, ate_alpha=0.05, control_name=0):
382
+ """Initialize an S-learner with a linear regression model.
383
+ Args:
384
+ ate_alpha (float, optional): the confidence level alpha of the ATE estimate
385
+ control_name (str or int, optional): name of control group
386
+ """
387
+ super().__init__(StatsmodelsOLS(alpha=ate_alpha), ate_alpha, control_name)
388
+
389
+ def estimate_ate(self, X, treatment, y, p=None, pretrain=False):
390
+ """Estimate the Average Treatment Effect (ATE).
391
+ Args:
392
+ X (np.matrix, np.array, or pd.Dataframe): a feature matrix
393
+ treatment (np.array or pd.Series): a treatment vector
394
+ y (np.array or pd.Series): an outcome vector
395
+ Returns:
396
+ The mean and confidence interval (LB, UB) of the ATE estimate.
397
+ """
398
+ X, treatment, y = convert_pd_to_np(X, treatment, y)
399
+ if not pretrain:
400
+ self.fit(X, treatment, y)
401
+
402
+ ate = np.zeros(self.t_groups.shape[0])
403
+ ate_lb = np.zeros(self.t_groups.shape[0])
404
+ ate_ub = np.zeros(self.t_groups.shape[0])
405
+
406
+ for i, group in enumerate(self.t_groups):
407
+ ate[i] = self.models[group].coefficients[0]
408
+ ate_lb[i] = self.models[group].conf_ints[0, 0]
409
+ ate_ub[i] = self.models[group].conf_ints[0, 1]
410
+
411
+ return ate, ate_lb, ate_ub
causalml/source/causalml/inference/meta/tlearner.py ADDED
@@ -0,0 +1,423 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from copy import deepcopy
2
+ import logging
3
+ import numpy as np
4
+ from packaging import version
5
+ from scipy.stats import norm
6
+ import sklearn
7
+ from sklearn.exceptions import ConvergenceWarning
8
+ from sklearn.neural_network import MLPRegressor
9
+
10
+ if version.parse(sklearn.__version__) >= version.parse("0.22.0"):
11
+ from sklearn.utils._testing import ignore_warnings
12
+ else:
13
+ from sklearn.utils.testing import ignore_warnings
14
+ from tqdm import tqdm
15
+ from xgboost import XGBRegressor
16
+
17
+ from causalml.inference.meta.base import BaseLearner
18
+ from causalml.inference.meta.utils import check_treatment_vector, convert_pd_to_np
19
+ from causalml.metrics import regression_metrics, classification_metrics
20
+
21
+ logger = logging.getLogger("causalml")
22
+
23
+
24
+ class BaseTLearner(BaseLearner):
25
+ """A parent class for T-learner regressor classes.
26
+
27
+ A T-learner estimates treatment effects with two machine learning models.
28
+
29
+ Details of T-learner are available at `Kunzel et al. (2018) <https://arxiv.org/abs/1706.03461>`_.
30
+ """
31
+
32
+ def __init__(
33
+ self,
34
+ learner=None,
35
+ control_learner=None,
36
+ treatment_learner=None,
37
+ ate_alpha=0.05,
38
+ control_name=0,
39
+ ):
40
+ """Initialize a T-learner.
41
+
42
+ Args:
43
+ learner (model): a model to estimate control and treatment outcomes.
44
+ control_learner (model, optional): a model to estimate control outcomes
45
+ treatment_learner (model, optional): a model to estimate treatment outcomes
46
+ ate_alpha (float, optional): the confidence level alpha of the ATE estimate
47
+ control_name (str or int, optional): name of control group
48
+ """
49
+ assert (learner is not None) or (
50
+ (control_learner is not None) and (treatment_learner is not None)
51
+ )
52
+
53
+ if control_learner is None:
54
+ self.model_c = deepcopy(learner)
55
+ else:
56
+ self.model_c = control_learner
57
+
58
+ if treatment_learner is None:
59
+ self.model_t = deepcopy(learner)
60
+ else:
61
+ self.model_t = treatment_learner
62
+
63
+ self.ate_alpha = ate_alpha
64
+ self.control_name = control_name
65
+
66
+ def __repr__(self):
67
+ return "{}(model_c={}, model_t={})".format(
68
+ self.__class__.__name__, self.model_c.__repr__(), self.model_t.__repr__()
69
+ )
70
+
71
+ @ignore_warnings(category=ConvergenceWarning)
72
+ def fit(self, X, treatment, y, p=None):
73
+ """Fit the inference model
74
+
75
+ Args:
76
+ X (np.matrix or np.array or pd.Dataframe): a feature matrix
77
+ treatment (np.array or pd.Series): a treatment vector
78
+ y (np.array or pd.Series): an outcome vector
79
+ """
80
+ X, treatment, y = convert_pd_to_np(X, treatment, y)
81
+ check_treatment_vector(treatment, self.control_name)
82
+ self.t_groups = np.unique(treatment[treatment != self.control_name])
83
+ self.t_groups.sort()
84
+ self._classes = {group: i for i, group in enumerate(self.t_groups)}
85
+ self.models_c = {group: deepcopy(self.model_c) for group in self.t_groups}
86
+ self.models_t = {group: deepcopy(self.model_t) for group in self.t_groups}
87
+
88
+ for group in self.t_groups:
89
+ mask = (treatment == group) | (treatment == self.control_name)
90
+ treatment_filt = treatment[mask]
91
+ X_filt = X[mask]
92
+ y_filt = y[mask]
93
+ w = (treatment_filt == group).astype(int)
94
+
95
+ self.models_c[group].fit(X_filt[w == 0], y_filt[w == 0])
96
+ self.models_t[group].fit(X_filt[w == 1], y_filt[w == 1])
97
+
98
+ def predict(
99
+ self, X, treatment=None, y=None, p=None, return_components=False, verbose=True
100
+ ):
101
+ """Predict treatment effects.
102
+
103
+ Args:
104
+ X (np.matrix or np.array or pd.Dataframe): a feature matrix
105
+ treatment (np.array or pd.Series, optional): a treatment vector
106
+ y (np.array or pd.Series, optional): an outcome vector
107
+ return_components (bool, optional): whether to return outcome for treatment and control seperately
108
+ verbose (bool, optional): whether to output progress logs
109
+ Returns:
110
+ (numpy.ndarray): Predictions of treatment effects.
111
+ """
112
+ X, treatment, y = convert_pd_to_np(X, treatment, y)
113
+ yhat_cs = {}
114
+ yhat_ts = {}
115
+
116
+ for group in self.t_groups:
117
+ model_c = self.models_c[group]
118
+ model_t = self.models_t[group]
119
+ yhat_cs[group] = model_c.predict(X)
120
+ yhat_ts[group] = model_t.predict(X)
121
+
122
+ if (y is not None) and (treatment is not None) and verbose:
123
+ mask = (treatment == group) | (treatment == self.control_name)
124
+ treatment_filt = treatment[mask]
125
+ y_filt = y[mask]
126
+ w = (treatment_filt == group).astype(int)
127
+
128
+ yhat = np.zeros_like(y_filt, dtype=float)
129
+ yhat[w == 0] = yhat_cs[group][mask][w == 0]
130
+ yhat[w == 1] = yhat_ts[group][mask][w == 1]
131
+
132
+ logger.info("Error metrics for group {}".format(group))
133
+ regression_metrics(y_filt, yhat, w)
134
+
135
+ te = np.zeros((X.shape[0], self.t_groups.shape[0]))
136
+ for i, group in enumerate(self.t_groups):
137
+ te[:, i] = yhat_ts[group] - yhat_cs[group]
138
+
139
+ if not return_components:
140
+ return te
141
+ else:
142
+ return te, yhat_cs, yhat_ts
143
+
144
+ def fit_predict(
145
+ self,
146
+ X,
147
+ treatment,
148
+ y,
149
+ p=None,
150
+ return_ci=False,
151
+ n_bootstraps=1000,
152
+ bootstrap_size=10000,
153
+ return_components=False,
154
+ verbose=True,
155
+ ):
156
+ """Fit the inference model of the T learner and predict treatment effects.
157
+
158
+ Args:
159
+ X (np.matrix or np.array or pd.Dataframe): a feature matrix
160
+ treatment (np.array or pd.Series): a treatment vector
161
+ y (np.array or pd.Series): an outcome vector
162
+ return_ci (bool): whether to return confidence intervals
163
+ n_bootstraps (int): number of bootstrap iterations
164
+ bootstrap_size (int): number of samples per bootstrap
165
+ return_components (bool, optional): whether to return outcome for treatment and control seperately
166
+ verbose (str): whether to output progress logs
167
+ Returns:
168
+ (numpy.ndarray): Predictions of treatment effects. Output dim: [n_samples, n_treatment].
169
+ If return_ci, returns CATE [n_samples, n_treatment], LB [n_samples, n_treatment],
170
+ UB [n_samples, n_treatment]
171
+ """
172
+ X, treatment, y = convert_pd_to_np(X, treatment, y)
173
+ self.fit(X, treatment, y)
174
+ te = self.predict(X, treatment, y, return_components=return_components)
175
+
176
+ if not return_ci:
177
+ return te
178
+ else:
179
+ t_groups_global = self.t_groups
180
+ _classes_global = self._classes
181
+ models_c_global = deepcopy(self.models_c)
182
+ models_t_global = deepcopy(self.models_t)
183
+ te_bootstraps = np.zeros(
184
+ shape=(X.shape[0], self.t_groups.shape[0], n_bootstraps)
185
+ )
186
+
187
+ logger.info("Bootstrap Confidence Intervals")
188
+ for i in tqdm(range(n_bootstraps)):
189
+ te_b = self.bootstrap(X, treatment, y, size=bootstrap_size)
190
+ te_bootstraps[:, :, i] = te_b
191
+
192
+ te_lower = np.percentile(te_bootstraps, (self.ate_alpha / 2) * 100, axis=2)
193
+ te_upper = np.percentile(
194
+ te_bootstraps, (1 - self.ate_alpha / 2) * 100, axis=2
195
+ )
196
+
197
+ # set member variables back to global (currently last bootstrapped outcome)
198
+ self.t_groups = t_groups_global
199
+ self._classes = _classes_global
200
+ self.models_c = deepcopy(models_c_global)
201
+ self.models_t = deepcopy(models_t_global)
202
+
203
+ return (te, te_lower, te_upper)
204
+
205
+ def estimate_ate(
206
+ self,
207
+ X,
208
+ treatment,
209
+ y,
210
+ p=None,
211
+ bootstrap_ci=False,
212
+ n_bootstraps=1000,
213
+ bootstrap_size=10000,
214
+ pretrain=False,
215
+ ):
216
+ """Estimate the Average Treatment Effect (ATE).
217
+
218
+ Args:
219
+ X (np.matrix or np.array or pd.Dataframe): a feature matrix
220
+ treatment (np.array or pd.Series): a treatment vector
221
+ y (np.array or pd.Series): an outcome vector
222
+ bootstrap_ci (bool): whether to return confidence intervals
223
+ n_bootstraps (int): number of bootstrap iterations
224
+ bootstrap_size (int): number of samples per bootstrap
225
+ Returns:
226
+ The mean and confidence interval (LB, UB) of the ATE estimate.
227
+ pretrain (bool): whether a model has been fit, default False.
228
+ """
229
+ X, treatment, y = convert_pd_to_np(X, treatment, y)
230
+ if pretrain:
231
+ te, yhat_cs, yhat_ts = self.predict(X, treatment, y, return_components=True)
232
+ else:
233
+ te, yhat_cs, yhat_ts = self.fit_predict(
234
+ X, treatment, y, return_components=True
235
+ )
236
+
237
+ ate = np.zeros(self.t_groups.shape[0])
238
+ ate_lb = np.zeros(self.t_groups.shape[0])
239
+ ate_ub = np.zeros(self.t_groups.shape[0])
240
+
241
+ for i, group in enumerate(self.t_groups):
242
+ _ate = te[:, i].mean()
243
+
244
+ mask = (treatment == group) | (treatment == self.control_name)
245
+ treatment_filt = treatment[mask]
246
+ y_filt = y[mask]
247
+ w = (treatment_filt == group).astype(int)
248
+ prob_treatment = float(sum(w)) / w.shape[0]
249
+
250
+ yhat_c = yhat_cs[group][mask]
251
+ yhat_t = yhat_ts[group][mask]
252
+
253
+ se = np.sqrt(
254
+ (
255
+ (y_filt[w == 0] - yhat_c[w == 0]).var() / (1 - prob_treatment)
256
+ + (y_filt[w == 1] - yhat_t[w == 1]).var() / prob_treatment
257
+ + (yhat_t - yhat_c).var()
258
+ )
259
+ / y_filt.shape[0]
260
+ )
261
+
262
+ _ate_lb = _ate - se * norm.ppf(1 - self.ate_alpha / 2)
263
+ _ate_ub = _ate + se * norm.ppf(1 - self.ate_alpha / 2)
264
+
265
+ ate[i] = _ate
266
+ ate_lb[i] = _ate_lb
267
+ ate_ub[i] = _ate_ub
268
+
269
+ if not bootstrap_ci:
270
+ return ate, ate_lb, ate_ub
271
+ else:
272
+ t_groups_global = self.t_groups
273
+ _classes_global = self._classes
274
+ models_c_global = deepcopy(self.models_c)
275
+ models_t_global = deepcopy(self.models_t)
276
+
277
+ logger.info("Bootstrap Confidence Intervals for ATE")
278
+ ate_bootstraps = np.zeros(shape=(self.t_groups.shape[0], n_bootstraps))
279
+
280
+ for n in tqdm(range(n_bootstraps)):
281
+ ate_b = self.bootstrap(X, treatment, y, size=bootstrap_size)
282
+ ate_bootstraps[:, n] = ate_b.mean(axis=0)
283
+
284
+ ate_lower = np.percentile(
285
+ ate_bootstraps, (self.ate_alpha / 2) * 100, axis=1
286
+ )
287
+ ate_upper = np.percentile(
288
+ ate_bootstraps, (1 - self.ate_alpha / 2) * 100, axis=1
289
+ )
290
+
291
+ # set member variables back to global (currently last bootstrapped outcome)
292
+ self.t_groups = t_groups_global
293
+ self._classes = _classes_global
294
+ self.models_c = deepcopy(models_c_global)
295
+ self.models_t = deepcopy(models_t_global)
296
+
297
+ return ate, ate_lower, ate_upper
298
+
299
+
300
+ class BaseTRegressor(BaseTLearner):
301
+ """
302
+ A parent class for T-learner regressor classes.
303
+ """
304
+
305
+ def __init__(
306
+ self,
307
+ learner=None,
308
+ control_learner=None,
309
+ treatment_learner=None,
310
+ ate_alpha=0.05,
311
+ control_name=0,
312
+ ):
313
+ """Initialize a T-learner regressor.
314
+
315
+ Args:
316
+ learner (model): a model to estimate control and treatment outcomes.
317
+ control_learner (model, optional): a model to estimate control outcomes
318
+ treatment_learner (model, optional): a model to estimate treatment outcomes
319
+ ate_alpha (float, optional): the confidence level alpha of the ATE estimate
320
+ control_name (str or int, optional): name of control group
321
+ """
322
+ super().__init__(
323
+ learner=learner,
324
+ control_learner=control_learner,
325
+ treatment_learner=treatment_learner,
326
+ ate_alpha=ate_alpha,
327
+ control_name=control_name,
328
+ )
329
+
330
+
331
+ class BaseTClassifier(BaseTLearner):
332
+ """
333
+ A parent class for T-learner classifier classes.
334
+ """
335
+
336
+ def __init__(
337
+ self,
338
+ learner=None,
339
+ control_learner=None,
340
+ treatment_learner=None,
341
+ ate_alpha=0.05,
342
+ control_name=0,
343
+ ):
344
+ """Initialize a T-learner classifier.
345
+
346
+ Args:
347
+ learner (model): a model to estimate control and treatment outcomes.
348
+ control_learner (model, optional): a model to estimate control outcomes
349
+ treatment_learner (model, optional): a model to estimate treatment outcomes
350
+ ate_alpha (float, optional): the confidence level alpha of the ATE estimate
351
+ control_name (str or int, optional): name of control group
352
+ """
353
+ super().__init__(
354
+ learner=learner,
355
+ control_learner=control_learner,
356
+ treatment_learner=treatment_learner,
357
+ ate_alpha=ate_alpha,
358
+ control_name=control_name,
359
+ )
360
+
361
+ def predict(
362
+ self, X, treatment=None, y=None, p=None, return_components=False, verbose=True
363
+ ):
364
+ """Predict treatment effects.
365
+
366
+ Args:
367
+ X (np.matrix or np.array or pd.Dataframe): a feature matrix
368
+ treatment (np.array or pd.Series, optional): a treatment vector
369
+ y (np.array or pd.Series, optional): an outcome vector
370
+ verbose (bool, optional): whether to output progress logs
371
+ Returns:
372
+ (numpy.ndarray): Predictions of treatment effects.
373
+ """
374
+ yhat_cs = {}
375
+ yhat_ts = {}
376
+
377
+ for group in self.t_groups:
378
+ model_c = self.models_c[group]
379
+ model_t = self.models_t[group]
380
+ yhat_cs[group] = model_c.predict_proba(X)[:, 1]
381
+ yhat_ts[group] = model_t.predict_proba(X)[:, 1]
382
+
383
+ if (y is not None) and (treatment is not None) and verbose:
384
+ mask = (treatment == group) | (treatment == self.control_name)
385
+ treatment_filt = treatment[mask]
386
+ y_filt = y[mask]
387
+ w = (treatment_filt == group).astype(int)
388
+
389
+ yhat = np.zeros_like(y_filt, dtype=float)
390
+ yhat[w == 0] = yhat_cs[group][mask][w == 0]
391
+ yhat[w == 1] = yhat_ts[group][mask][w == 1]
392
+
393
+ logger.info("Error metrics for group {}".format(group))
394
+ classification_metrics(y_filt, yhat, w)
395
+
396
+ te = np.zeros((X.shape[0], self.t_groups.shape[0]))
397
+ for i, group in enumerate(self.t_groups):
398
+ te[:, i] = yhat_ts[group] - yhat_cs[group]
399
+
400
+ if not return_components:
401
+ return te
402
+ else:
403
+ return te, yhat_cs, yhat_ts
404
+
405
+
406
+ class XGBTRegressor(BaseTRegressor):
407
+ def __init__(self, ate_alpha=0.05, control_name=0, *args, **kwargs):
408
+ """Initialize a T-learner with two XGBoost models."""
409
+ super().__init__(
410
+ learner=XGBRegressor(*args, **kwargs),
411
+ ate_alpha=ate_alpha,
412
+ control_name=control_name,
413
+ )
414
+
415
+
416
+ class MLPTRegressor(BaseTRegressor):
417
+ def __init__(self, ate_alpha=0.05, control_name=0, *args, **kwargs):
418
+ """Initialize a T-learner with two MLP models."""
419
+ super().__init__(
420
+ learner=MLPRegressor(*args, **kwargs),
421
+ ate_alpha=ate_alpha,
422
+ control_name=control_name,
423
+ )