Spaces:
Sleeping
Sleeping
Upload 199 files
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .gitattributes +4 -0
- Dockerfile +18 -0
- README.md +27 -5
- app.py +45 -0
- causalml/mcp_output/README_MCP.md +73 -0
- causalml/mcp_output/analysis.json +459 -0
- causalml/mcp_output/diff_report.md +60 -0
- causalml/mcp_output/mcp_plugin/__init__.py +0 -0
- causalml/mcp_output/mcp_plugin/adapter.py +214 -0
- causalml/mcp_output/mcp_plugin/main.py +13 -0
- causalml/mcp_output/mcp_plugin/mcp_service.py +164 -0
- causalml/mcp_output/requirements.txt +26 -0
- causalml/mcp_output/start_mcp.py +30 -0
- causalml/mcp_output/workflow_summary.json +201 -0
- causalml/source/.pre-commit-config.yaml +11 -0
- causalml/source/.readthedocs.yml +25 -0
- causalml/source/ANTITRUST.md +7 -0
- causalml/source/CHARTER.md +49 -0
- causalml/source/CODE_OF_CONDUCT.md +75 -0
- causalml/source/CONTRIBUTING.md +134 -0
- causalml/source/GOVERNANCE.md +54 -0
- causalml/source/LICENSE +13 -0
- causalml/source/MAINTAINERS.md +27 -0
- causalml/source/MANIFEST.in +7 -0
- causalml/source/Makefile +27 -0
- causalml/source/README.md +132 -0
- causalml/source/SECURITY.md +11 -0
- causalml/source/STEERING_COMMITTEE.md +14 -0
- causalml/source/TRADEMARKS.md +44 -0
- causalml/source/__init__.py +4 -0
- causalml/source/causalml/__init__.py +10 -0
- causalml/source/causalml/dataset/__init__.py +16 -0
- causalml/source/causalml/dataset/classification.py +692 -0
- causalml/source/causalml/dataset/regression.py +209 -0
- causalml/source/causalml/dataset/semiSynthetic.py +1056 -0
- causalml/source/causalml/dataset/synthetic.py +655 -0
- causalml/source/causalml/feature_selection/__init__.py +1 -0
- causalml/source/causalml/feature_selection/filters.py +663 -0
- causalml/source/causalml/features.py +267 -0
- causalml/source/causalml/inference/__init__.py +0 -0
- causalml/source/causalml/inference/iv/__init__.py +2 -0
- causalml/source/causalml/inference/iv/drivlearner.py +881 -0
- causalml/source/causalml/inference/iv/iv_regression.py +48 -0
- causalml/source/causalml/inference/meta/__init__.py +12 -0
- causalml/source/causalml/inference/meta/base.py +337 -0
- causalml/source/causalml/inference/meta/drlearner.py +592 -0
- causalml/source/causalml/inference/meta/explainer.py +278 -0
- causalml/source/causalml/inference/meta/rlearner.py +695 -0
- causalml/source/causalml/inference/meta/slearner.py +411 -0
- 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:
|
| 5 |
-
colorTo:
|
| 6 |
sdk: docker
|
|
|
|
|
|
|
| 7 |
pinned: false
|
| 8 |
---
|
| 9 |
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
[](https://pypi.org/project/causalml/)
|
| 8 |
+
[](https://github.com/uber/causalml/actions/workflows/python-test.yaml)
|
| 9 |
+
[](http://causalml.readthedocs.io/en/latest/?badge=latest)
|
| 10 |
+
[](https://pepy.tech/project/causalml)
|
| 11 |
+
[](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 |
+
)
|