CausalPy 0.2.1__tar.gz → 0.2.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- CausalPy-0.2.3/CONTRIBUTING.md +201 -0
- {CausalPy-0.2.1 → CausalPy-0.2.3}/CausalPy.egg-info/PKG-INFO +3 -3
- {CausalPy-0.2.1 → CausalPy-0.2.3}/CausalPy.egg-info/SOURCES.txt +9 -1
- {CausalPy-0.2.1 → CausalPy-0.2.3}/PKG-INFO +3 -3
- {CausalPy-0.2.1 → CausalPy-0.2.3}/README.md +1 -1
- CausalPy-0.2.3/causalpy/data_validation.py +135 -0
- {CausalPy-0.2.1 → CausalPy-0.2.3}/causalpy/pymc_experiments.py +18 -119
- {CausalPy-0.2.1 → CausalPy-0.2.3}/causalpy/pymc_models.py +4 -4
- {CausalPy-0.2.1 → CausalPy-0.2.3}/causalpy/skl_experiments.py +15 -4
- CausalPy-0.2.3/causalpy/tests/__init__.py +0 -0
- CausalPy-0.2.3/causalpy/tests/conftest.py +15 -0
- CausalPy-0.2.3/causalpy/tests/test_data_loading.py +32 -0
- CausalPy-0.2.3/causalpy/tests/test_integration_pymc_examples.py +499 -0
- CausalPy-0.2.3/causalpy/tests/test_integration_skl_examples.py +191 -0
- CausalPy-0.2.3/causalpy/tests/test_pymc_models.py +164 -0
- {CausalPy-0.2.1 → CausalPy-0.2.3}/causalpy/version.py +2 -1
- {CausalPy-0.2.1 → CausalPy-0.2.3}/pyproject.toml +3 -3
- {CausalPy-0.2.1 → CausalPy-0.2.3}/CausalPy.egg-info/dependency_links.txt +0 -0
- {CausalPy-0.2.1 → CausalPy-0.2.3}/CausalPy.egg-info/requires.txt +0 -0
- {CausalPy-0.2.1 → CausalPy-0.2.3}/CausalPy.egg-info/top_level.txt +0 -0
- {CausalPy-0.2.1 → CausalPy-0.2.3}/LICENSE +0 -0
- {CausalPy-0.2.1 → CausalPy-0.2.3}/causalpy/__init__.py +0 -0
- {CausalPy-0.2.1 → CausalPy-0.2.3}/causalpy/custom_exceptions.py +0 -0
- {CausalPy-0.2.1 → CausalPy-0.2.3}/causalpy/data/AJR2001.csv +0 -0
- {CausalPy-0.2.1 → CausalPy-0.2.3}/causalpy/data/GDP_in_dollars_billions.csv +0 -0
- {CausalPy-0.2.1 → CausalPy-0.2.3}/causalpy/data/__init__.py +0 -0
- {CausalPy-0.2.1 → CausalPy-0.2.3}/causalpy/data/ancova_generated.csv +0 -0
- {CausalPy-0.2.1 → CausalPy-0.2.3}/causalpy/data/banks.csv +0 -0
- {CausalPy-0.2.1 → CausalPy-0.2.3}/causalpy/data/datasets.py +0 -0
- {CausalPy-0.2.1 → CausalPy-0.2.3}/causalpy/data/deaths_and_temps_england_wales.csv +0 -0
- {CausalPy-0.2.1 → CausalPy-0.2.3}/causalpy/data/did.csv +0 -0
- {CausalPy-0.2.1 → CausalPy-0.2.3}/causalpy/data/drinking.csv +0 -0
- {CausalPy-0.2.1 → CausalPy-0.2.3}/causalpy/data/geolift1.csv +0 -0
- {CausalPy-0.2.1 → CausalPy-0.2.3}/causalpy/data/its.csv +0 -0
- {CausalPy-0.2.1 → CausalPy-0.2.3}/causalpy/data/its_simple.csv +0 -0
- {CausalPy-0.2.1 → CausalPy-0.2.3}/causalpy/data/regression_discontinuity.csv +0 -0
- {CausalPy-0.2.1 → CausalPy-0.2.3}/causalpy/data/simulate_data.py +0 -0
- {CausalPy-0.2.1 → CausalPy-0.2.3}/causalpy/data/synthetic_control.csv +0 -0
- {CausalPy-0.2.1 → CausalPy-0.2.3}/causalpy/plot_utils.py +0 -0
- {CausalPy-0.2.1 → CausalPy-0.2.3}/causalpy/skl_models.py +0 -0
- {CausalPy-0.2.1 → CausalPy-0.2.3}/causalpy/utils.py +0 -0
- {CausalPy-0.2.1 → CausalPy-0.2.3}/setup.cfg +0 -0
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
# Guidelines for Contributing
|
|
2
|
+
|
|
3
|
+
CausalPy welcomes contributions from interested individuals or groups. These guidelines are provided to give potential contributors information to make their contribution compliant with the conventions of the CausalPy project, and maximize the probability of such contributions are merged as quickly and efficiently as possible. Contributors need not be experts, but should be interested in the project, willing to learn, and share knowledge.
|
|
4
|
+
|
|
5
|
+
There are 4 main ways of contributing to the CausalPy project (in ascending order of difficulty or scope):
|
|
6
|
+
|
|
7
|
+
1. Submitting issues related to bugs or desired enhancements.
|
|
8
|
+
2. Contributing or improving the documentation (docs) or examples.
|
|
9
|
+
3. Fixing outstanding issues (bugs) with the existing codebase. They range from low-level software bugs to higher-level design problems.
|
|
10
|
+
4. Adding new or improved functionality to the existing codebase.
|
|
11
|
+
|
|
12
|
+
Items 2-4 require setting up a local development environment, see [Local development steps](#Local-development-steps) for more information.
|
|
13
|
+
|
|
14
|
+
## Opening issues
|
|
15
|
+
|
|
16
|
+
We appreciate being notified of problems with the existing CausalPy code. We prefer that issues be filed the on [Github Issue Tracker](https://github.com/pymc-labs/CausalPy/issues), rather than on social media or by direct email to the developers.
|
|
17
|
+
|
|
18
|
+
Please verify that your issue is not being currently addressed by other issues or pull requests by using the GitHub search tool to look for key words in the project issue tracker.
|
|
19
|
+
|
|
20
|
+
## Contributing code via pull requests
|
|
21
|
+
|
|
22
|
+
While issue reporting is valuable, we strongly encourage users who are inclined to do so to submit patches for new or existing issues via pull requests. This is particularly the case for simple fixes, such as typos or tweaks to documentation, which do not require a heavy investment of time and attention.
|
|
23
|
+
|
|
24
|
+
Contributors are also encouraged to contribute new code to enhance CausalPy's functionality, via pull requests.
|
|
25
|
+
|
|
26
|
+
The preferred workflow for contributing to CausalPy is to fork the GitHub repository, clone it to your local machine, and develop on a feature branch.
|
|
27
|
+
|
|
28
|
+
For more instructions see the [Pull request checklist](#pull-request-checklist)
|
|
29
|
+
|
|
30
|
+
## Local development steps
|
|
31
|
+
|
|
32
|
+
1. If you have not already done so, fork the [project repository](https://github.com/pymc-labs/CausalPy) by clicking on the 'Fork' button near the top right of the main repository page. This creates a copy of the code under your GitHub user account.
|
|
33
|
+
|
|
34
|
+
1. Clone your fork of the `CausalPy` repo from your GitHub account to your local disk, and add the base repository as a remote:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
git clone git@github.com:<your GitHub handle>/CausalPy.git
|
|
38
|
+
cd CausalPy
|
|
39
|
+
git remote add upstream git@github.com:pymc-labs/CausalPy.git
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
1. Create a feature branch (e.g. `my-feature`) to hold your development changes:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
git checkout -b my-feature
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Always use a feature branch. It's good practice to never routinely work on the `main` branch of any repository.
|
|
49
|
+
|
|
50
|
+
1. Create a new environment using Python >=3.10, for example 3.11
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
conda create --name CausalPy python=3.11
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Activate the environment.
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
conda activate CausalPy
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Install the package (in editable mode) and its development dependencies:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
pip install --no-deps -e .
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Install development dependencies
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
pip install 'causalpy[dev]'
|
|
72
|
+
pip install 'causalpy[docs]'
|
|
73
|
+
pip install 'causalpy[test]'
|
|
74
|
+
pip install 'causalpy[lint]'
|
|
75
|
+
pip install 'pylint'
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
It may also be necessary to [install](https://pandoc.org/installing.html) `pandoc`. On a mac, run `brew install pandoc`.
|
|
79
|
+
|
|
80
|
+
Set [pre-commit hooks](https://pre-commit.com/)
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
pre-commit install
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
If you are editing or writing new examples in the form of Jupyter notebooks, you may have to run the following command to make Jupyter Lab aware of the `CausalPy` environment.
|
|
87
|
+
|
|
88
|
+
```
|
|
89
|
+
python -m ipykernel install --user --name CausalPy
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
1. You can then work on your changes locally, in your feature branch. Add changed files using `git add` and then `git commit` files:
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
git add modified_files
|
|
96
|
+
git commit -m "Message summarizing commit changes"
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
to record your changes locally.
|
|
100
|
+
After committing, it is a good idea to sync with the base repository in case there have been any changes:
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
git fetch upstream
|
|
104
|
+
git rebase upstream/main
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Then push the changes to your GitHub account with:
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
git push -u origin my-feature
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
1. Before you submit a Pull Request, follow the [Pull request checklist](#pull-request-checklist).
|
|
114
|
+
|
|
115
|
+
1. Finally, to submit a pull request, go to the GitHub web page of your fork of the CausalPy repo. Click the 'Pull request' button to send your changes to the project's maintainers for review. This will send an email to the committers.
|
|
116
|
+
|
|
117
|
+
## Pull request checklist
|
|
118
|
+
|
|
119
|
+
We recommend that your contribution complies with the following guidelines before you submit a pull request:
|
|
120
|
+
|
|
121
|
+
- If your pull request addresses an issue, please use the pull request title to describe the issue and mention the issue number in the pull request description. This will make sure a link back to the original issue is created.
|
|
122
|
+
|
|
123
|
+
- All public methods must have informative docstrings with sample usage when appropriate.
|
|
124
|
+
|
|
125
|
+
- Example usage in docstrings is tested via doctest, which can be run via
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
make doctest
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
- Doctest can also be run directly via pytest, which can be helpful to run only specific tests during development. The following commands run all doctests, only doctests in the pymc_models module, and only the doctests for the `ModelBuilder` class in pymc_models:
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
pytest --doctest-modules causalpy/
|
|
135
|
+
pytest --doctest-modules causalpy/pymc_models.py
|
|
136
|
+
pytest --doctest-modules causalpy/pmyc_models.py::causalpy.pymc_models.ModelBuilder
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
- To indicate a work in progress please mark the PR as `draft`. Drafts may be useful to (1) indicate you are working on something to avoid duplicated work, (2) request broad review of functionality or API, or (3) seek collaborators.
|
|
140
|
+
|
|
141
|
+
- All other tests pass when everything is rebuilt from scratch. Tests can be run with:
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
make test
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
- When adding additional functionality, either edit an existing example, or create a new example (typically in the form of a Jupyter Notebook). Have a look at other examples for reference. Examples should demonstrate why the new functionality is useful in practice.
|
|
148
|
+
|
|
149
|
+
- Documentation and high-coverage tests are necessary for enhancements to be accepted.
|
|
150
|
+
|
|
151
|
+
- Documentation follows [NumPy style guide](https://numpydoc.readthedocs.io/en/latest/format.html)
|
|
152
|
+
|
|
153
|
+
- If you have changed the documentation, you should [build the docs locally](#Building-the-documentation-locally) and check that the changes look correct.
|
|
154
|
+
|
|
155
|
+
- Run any of the pre-existing examples in `CausalPy/docs/source/*` that contain analyses that would be affected by your changes to ensure that nothing breaks. This is a useful opportunity to not only check your work for bugs that might not be revealed by unit test, but also to show how your contribution improves CausalPy for end users.
|
|
156
|
+
|
|
157
|
+
- Your code passes linting tests. Run the line below to check linting errors:
|
|
158
|
+
|
|
159
|
+
```bash
|
|
160
|
+
make check_lint
|
|
161
|
+
```
|
|
162
|
+
If you want to fix linting errors automatically, run
|
|
163
|
+
|
|
164
|
+
```bash
|
|
165
|
+
make lint
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
## Building the documentation locally
|
|
169
|
+
|
|
170
|
+
A local build of the docs is achieved by:
|
|
171
|
+
|
|
172
|
+
```bash
|
|
173
|
+
cd docs
|
|
174
|
+
make html
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
Sometimes not all changes are recognised. In that case run this (again from within the `docs` folder):
|
|
178
|
+
|
|
179
|
+
```bash
|
|
180
|
+
make clean && make html
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
Docs are built in `docs/_build`, but these docs are _not_ committed to the GitHub repository due to `.gitignore`.
|
|
184
|
+
|
|
185
|
+
## Overview of code structure
|
|
186
|
+
|
|
187
|
+
Classes
|
|
188
|
+

|
|
189
|
+
|
|
190
|
+
Packages
|
|
191
|
+

|
|
192
|
+
|
|
193
|
+
UML diagrams can be created with the command below.
|
|
194
|
+
|
|
195
|
+
```bash
|
|
196
|
+
make uml
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
---
|
|
200
|
+
|
|
201
|
+
This guide takes some inspiration from the [Bambi guide to contributing](https://github.com/bambinos/bambi/blob/main/docs/CONTRIBUTING.md)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: CausalPy
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.3
|
|
4
4
|
Summary: Causal inference for quasi-experiments in Python
|
|
5
5
|
Author-email: Ben Vincent <ben.vincent@pymc-labs.com>
|
|
6
6
|
License: Apache License
|
|
@@ -208,7 +208,7 @@ License: Apache License
|
|
|
208
208
|
Project-URL: Homepage, https://github.com/pymc-labs/CausalPy
|
|
209
209
|
Project-URL: Bug Reports, https://github.com/pymc-labs/CausalPy/issues
|
|
210
210
|
Project-URL: Source, https://github.com/pymc-labs/CausalPy
|
|
211
|
-
Requires-Python: >=3.
|
|
211
|
+
Requires-Python: >=3.10
|
|
212
212
|
Description-Content-Type: text/markdown
|
|
213
213
|
License-File: LICENSE
|
|
214
214
|
Requires-Dist: arviz>=0.14.0
|
|
@@ -430,7 +430,7 @@ Interrupted time series analysis is appropriate when you have a time series of o
|
|
|
430
430
|
|
|
431
431
|
| Frequentist | Bayesian |
|
|
432
432
|
|--|--|
|
|
433
|
-
|
|
|
433
|
+
|  |  |
|
|
434
434
|
|
|
435
435
|
> The data, pre-treatment model fit, and counterfactual are plotted (top). The causal impact is shown as a blue shaded region. The Bayesian analysis shows shaded Bayesian credible regions of the model fit and counterfactual. Also shown is the causal impact (middle) and cumulative causal impact (bottom).
|
|
436
436
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
CONTRIBUTING.md
|
|
1
2
|
LICENSE
|
|
2
3
|
README.md
|
|
3
4
|
pyproject.toml
|
|
@@ -8,6 +9,7 @@ CausalPy.egg-info/requires.txt
|
|
|
8
9
|
CausalPy.egg-info/top_level.txt
|
|
9
10
|
causalpy/__init__.py
|
|
10
11
|
causalpy/custom_exceptions.py
|
|
12
|
+
causalpy/data_validation.py
|
|
11
13
|
causalpy/plot_utils.py
|
|
12
14
|
causalpy/pymc_experiments.py
|
|
13
15
|
causalpy/pymc_models.py
|
|
@@ -29,4 +31,10 @@ causalpy/data/its.csv
|
|
|
29
31
|
causalpy/data/its_simple.csv
|
|
30
32
|
causalpy/data/regression_discontinuity.csv
|
|
31
33
|
causalpy/data/simulate_data.py
|
|
32
|
-
causalpy/data/synthetic_control.csv
|
|
34
|
+
causalpy/data/synthetic_control.csv
|
|
35
|
+
causalpy/tests/__init__.py
|
|
36
|
+
causalpy/tests/conftest.py
|
|
37
|
+
causalpy/tests/test_data_loading.py
|
|
38
|
+
causalpy/tests/test_integration_pymc_examples.py
|
|
39
|
+
causalpy/tests/test_integration_skl_examples.py
|
|
40
|
+
causalpy/tests/test_pymc_models.py
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: CausalPy
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.3
|
|
4
4
|
Summary: Causal inference for quasi-experiments in Python
|
|
5
5
|
Author-email: Ben Vincent <ben.vincent@pymc-labs.com>
|
|
6
6
|
License: Apache License
|
|
@@ -208,7 +208,7 @@ License: Apache License
|
|
|
208
208
|
Project-URL: Homepage, https://github.com/pymc-labs/CausalPy
|
|
209
209
|
Project-URL: Bug Reports, https://github.com/pymc-labs/CausalPy/issues
|
|
210
210
|
Project-URL: Source, https://github.com/pymc-labs/CausalPy
|
|
211
|
-
Requires-Python: >=3.
|
|
211
|
+
Requires-Python: >=3.10
|
|
212
212
|
Description-Content-Type: text/markdown
|
|
213
213
|
License-File: LICENSE
|
|
214
214
|
Requires-Dist: arviz>=0.14.0
|
|
@@ -430,7 +430,7 @@ Interrupted time series analysis is appropriate when you have a time series of o
|
|
|
430
430
|
|
|
431
431
|
| Frequentist | Bayesian |
|
|
432
432
|
|--|--|
|
|
433
|
-
|
|
|
433
|
+
|  |  |
|
|
434
434
|
|
|
435
435
|
> The data, pre-treatment model fit, and counterfactual are plotted (top). The causal impact is shown as a blue shaded region. The Bayesian analysis shows shaded Bayesian credible regions of the model fit and counterfactual. Also shown is the causal impact (middle) and cumulative causal impact (bottom).
|
|
436
436
|
|
|
@@ -178,7 +178,7 @@ Interrupted time series analysis is appropriate when you have a time series of o
|
|
|
178
178
|
|
|
179
179
|
| Frequentist | Bayesian |
|
|
180
180
|
|--|--|
|
|
181
|
-
|
|
|
181
|
+
|  |  |
|
|
182
182
|
|
|
183
183
|
> The data, pre-treatment model fit, and counterfactual are plotted (top). The causal impact is shown as a blue shaded region. The Bayesian analysis shows shaded Bayesian credible regions of the model fit and counterfactual. Also shown is the causal impact (middle) and cumulative causal impact (bottom).
|
|
184
184
|
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import warnings # noqa: I001
|
|
2
|
+
|
|
3
|
+
import pandas as pd
|
|
4
|
+
import numpy as np
|
|
5
|
+
from causalpy.custom_exceptions import (
|
|
6
|
+
BadIndexException, # NOQA
|
|
7
|
+
DataException,
|
|
8
|
+
FormulaException,
|
|
9
|
+
)
|
|
10
|
+
from causalpy.utils import _is_variable_dummy_coded
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class PrePostFitDataValidator:
|
|
14
|
+
"""Mixin class for validating the input data and model formula for PrePostFit"""
|
|
15
|
+
|
|
16
|
+
def _input_validation(self, data, treatment_time):
|
|
17
|
+
"""Validate the input data and model formula for correctness"""
|
|
18
|
+
if isinstance(data.index, pd.DatetimeIndex) and not isinstance(
|
|
19
|
+
treatment_time, pd.Timestamp
|
|
20
|
+
):
|
|
21
|
+
raise BadIndexException(
|
|
22
|
+
"If data.index is DatetimeIndex, treatment_time must be pd.Timestamp."
|
|
23
|
+
)
|
|
24
|
+
if not isinstance(data.index, pd.DatetimeIndex) and isinstance(
|
|
25
|
+
treatment_time, pd.Timestamp
|
|
26
|
+
):
|
|
27
|
+
raise BadIndexException(
|
|
28
|
+
"If data.index is not DatetimeIndex, treatment_time must be pd.Timestamp." # noqa: E501
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class DiDDataValidator:
|
|
33
|
+
"""Mixin class for validating the input data and model formula for Difference in Differences experiments."""
|
|
34
|
+
|
|
35
|
+
def _input_validation(self):
|
|
36
|
+
"""Validate the input data and model formula for correctness"""
|
|
37
|
+
if "post_treatment" not in self.formula:
|
|
38
|
+
raise FormulaException(
|
|
39
|
+
"A predictor called `post_treatment` should be in the formula"
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
if "post_treatment" not in self.data.columns:
|
|
43
|
+
raise DataException(
|
|
44
|
+
"Require a boolean column labelling observations which are `treated`"
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
if "unit" not in self.data.columns:
|
|
48
|
+
raise DataException(
|
|
49
|
+
"Require a `unit` column to label unique units. This is used for plotting purposes" # noqa: E501
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
if _is_variable_dummy_coded(self.data[self.group_variable_name]) is False:
|
|
53
|
+
raise DataException(
|
|
54
|
+
f"""The grouping variable {self.group_variable_name} should be dummy
|
|
55
|
+
coded. Consisting of 0's and 1's only."""
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class RDDataValidator:
|
|
60
|
+
"""Mixin class for validating the input data and model formula for Regression Discontinuity experiments."""
|
|
61
|
+
|
|
62
|
+
def _input_validation(self):
|
|
63
|
+
"""Validate the input data and model formula for correctness"""
|
|
64
|
+
if "treated" not in self.formula:
|
|
65
|
+
raise FormulaException(
|
|
66
|
+
"A predictor called `treated` should be in the formula"
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
if _is_variable_dummy_coded(self.data["treated"]) is False:
|
|
70
|
+
raise DataException(
|
|
71
|
+
"""The treated variable should be dummy coded. Consisting of 0's and 1's only.""" # noqa: E501
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class RegressionKinkDataValidator:
|
|
76
|
+
"""Mixin class for validating the input data and model formula for Regression Kink experiments."""
|
|
77
|
+
|
|
78
|
+
def _input_validation(self):
|
|
79
|
+
"""Validate the input data and model formula for correctness"""
|
|
80
|
+
if "treated" not in self.formula:
|
|
81
|
+
raise FormulaException(
|
|
82
|
+
"A predictor called `treated` should be in the formula"
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
if _is_variable_dummy_coded(self.data["treated"]) is False:
|
|
86
|
+
raise DataException(
|
|
87
|
+
"""The treated variable should be dummy coded. Consisting of 0's and 1's only.""" # noqa: E501
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
if self.bandwidth <= 0:
|
|
91
|
+
raise ValueError("The bandwidth must be greater than zero.")
|
|
92
|
+
|
|
93
|
+
if self.epsilon <= 0:
|
|
94
|
+
raise ValueError("Epsilon must be greater than zero.")
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class PrePostNEGDDataValidator:
|
|
98
|
+
"""Mixin class for validating the input data and model formula for PrePostNEGD experiments."""
|
|
99
|
+
|
|
100
|
+
def _input_validation(self) -> None:
|
|
101
|
+
"""Validate the input data and model formula for correctness"""
|
|
102
|
+
if not _is_variable_dummy_coded(self.data[self.group_variable_name]):
|
|
103
|
+
raise DataException(
|
|
104
|
+
f"""
|
|
105
|
+
There must be 2 levels of the grouping variable
|
|
106
|
+
{self.group_variable_name}. I.e. the treated and untreated.
|
|
107
|
+
"""
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class IVDataValidator:
|
|
112
|
+
"""Mixin class for validating the input data and model formula for IV experiments."""
|
|
113
|
+
|
|
114
|
+
def _input_validation(self):
|
|
115
|
+
"""Validate the input data and model formula for correctness"""
|
|
116
|
+
treatment = self.instruments_formula.split("~")[0]
|
|
117
|
+
test = treatment.strip() in self.instruments_data.columns
|
|
118
|
+
test = test & (treatment.strip() in self.data.columns)
|
|
119
|
+
if not test:
|
|
120
|
+
raise DataException(
|
|
121
|
+
f"""
|
|
122
|
+
The treatment variable:
|
|
123
|
+
{treatment} must appear in the instrument_data to be used
|
|
124
|
+
as an outcome variable and in the data object to be used as a covariate.
|
|
125
|
+
"""
|
|
126
|
+
)
|
|
127
|
+
Z = self.data[treatment.strip()]
|
|
128
|
+
check_binary = len(np.unique(Z)) > 2
|
|
129
|
+
if check_binary:
|
|
130
|
+
warnings.warn(
|
|
131
|
+
"""Warning. The treatment variable is not Binary.
|
|
132
|
+
This is not necessarily a problem but it violates
|
|
133
|
+
the assumption of a simple IV experiment.
|
|
134
|
+
The coefficients should be interpreted appropriately."""
|
|
135
|
+
)
|
|
@@ -23,13 +23,16 @@ import xarray as xr
|
|
|
23
23
|
from patsy import build_design_matrices, dmatrices
|
|
24
24
|
from sklearn.linear_model import LinearRegression as sk_lin_reg
|
|
25
25
|
|
|
26
|
-
from causalpy.
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
26
|
+
from causalpy.data_validation import (
|
|
27
|
+
PrePostFitDataValidator,
|
|
28
|
+
DiDDataValidator,
|
|
29
|
+
RDDataValidator,
|
|
30
|
+
RegressionKinkDataValidator,
|
|
31
|
+
PrePostNEGDDataValidator,
|
|
32
|
+
IVDataValidator,
|
|
30
33
|
)
|
|
31
34
|
from causalpy.plot_utils import plot_xY
|
|
32
|
-
from causalpy.utils import
|
|
35
|
+
from causalpy.utils import round_num
|
|
33
36
|
|
|
34
37
|
LEGEND_FONT_SIZE = 12
|
|
35
38
|
az.style.use("arviz-darkgrid")
|
|
@@ -108,7 +111,7 @@ class ExperimentalDesign:
|
|
|
108
111
|
)
|
|
109
112
|
|
|
110
113
|
|
|
111
|
-
class PrePostFit(ExperimentalDesign):
|
|
114
|
+
class PrePostFit(ExperimentalDesign, PrePostFitDataValidator):
|
|
112
115
|
"""
|
|
113
116
|
A class to analyse quasi-experiments where parameter estimation is based on just
|
|
114
117
|
the pre-intervention data.
|
|
@@ -160,7 +163,6 @@ class PrePostFit(ExperimentalDesign):
|
|
|
160
163
|
) -> None:
|
|
161
164
|
super().__init__(model=model, **kwargs)
|
|
162
165
|
self._input_validation(data, treatment_time)
|
|
163
|
-
|
|
164
166
|
self.treatment_time = treatment_time
|
|
165
167
|
# set experiment type - usually done in subclasses
|
|
166
168
|
self.expt_type = "Pre-Post Fit"
|
|
@@ -202,33 +204,18 @@ class PrePostFit(ExperimentalDesign):
|
|
|
202
204
|
# causal impact pre (ie the residuals of the model fit to observed)
|
|
203
205
|
pre_data = xr.DataArray(self.pre_y[:, 0], dims=["obs_ind"])
|
|
204
206
|
self.pre_impact = (
|
|
205
|
-
pre_data - self.pre_pred["posterior_predictive"]
|
|
207
|
+
pre_data - self.pre_pred["posterior_predictive"]["y_hat"]
|
|
206
208
|
).transpose(..., "obs_ind")
|
|
207
209
|
|
|
208
210
|
# causal impact post (ie the residuals of the model fit to observed)
|
|
209
211
|
post_data = xr.DataArray(self.post_y[:, 0], dims=["obs_ind"])
|
|
210
212
|
self.post_impact = (
|
|
211
|
-
post_data - self.post_pred["posterior_predictive"]
|
|
213
|
+
post_data - self.post_pred["posterior_predictive"]["y_hat"]
|
|
212
214
|
).transpose(..., "obs_ind")
|
|
213
215
|
|
|
214
216
|
# cumulative impact post
|
|
215
217
|
self.post_impact_cumulative = self.post_impact.cumsum(dim="obs_ind")
|
|
216
218
|
|
|
217
|
-
def _input_validation(self, data, treatment_time):
|
|
218
|
-
"""Validate the input data and model formula for correctness"""
|
|
219
|
-
if isinstance(data.index, pd.DatetimeIndex) and not isinstance(
|
|
220
|
-
treatment_time, pd.Timestamp
|
|
221
|
-
):
|
|
222
|
-
raise BadIndexException(
|
|
223
|
-
"If data.index is DatetimeIndex, treatment_time must be pd.Timestamp."
|
|
224
|
-
)
|
|
225
|
-
if not isinstance(data.index, pd.DatetimeIndex) and isinstance(
|
|
226
|
-
treatment_time, pd.Timestamp
|
|
227
|
-
):
|
|
228
|
-
raise BadIndexException(
|
|
229
|
-
"If data.index is not DatetimeIndex, treatment_time must be pd.Timestamp." # noqa: E501
|
|
230
|
-
)
|
|
231
|
-
|
|
232
219
|
def plot(self, counterfactual_label="Counterfactual", round_to=None, **kwargs):
|
|
233
220
|
"""
|
|
234
221
|
Plot the results
|
|
@@ -438,7 +425,7 @@ class SyntheticControl(PrePostFit):
|
|
|
438
425
|
return fig, ax
|
|
439
426
|
|
|
440
427
|
|
|
441
|
-
class DifferenceInDifferences(ExperimentalDesign):
|
|
428
|
+
class DifferenceInDifferences(ExperimentalDesign, DiDDataValidator):
|
|
442
429
|
"""A class to analyse data from Difference in Difference settings.
|
|
443
430
|
|
|
444
431
|
.. note::
|
|
@@ -568,29 +555,6 @@ class DifferenceInDifferences(ExperimentalDesign):
|
|
|
568
555
|
if "post_treatment" in label and self.group_variable_name in label:
|
|
569
556
|
self.causal_impact = self.idata.posterior["beta"].isel({"coeffs": i})
|
|
570
557
|
|
|
571
|
-
def _input_validation(self):
|
|
572
|
-
"""Validate the input data and model formula for correctness"""
|
|
573
|
-
if "post_treatment" not in self.formula:
|
|
574
|
-
raise FormulaException(
|
|
575
|
-
"A predictor called `post_treatment` should be in the formula"
|
|
576
|
-
)
|
|
577
|
-
|
|
578
|
-
if "post_treatment" not in self.data.columns:
|
|
579
|
-
raise DataException(
|
|
580
|
-
"Require a boolean column labelling observations which are `treated`"
|
|
581
|
-
)
|
|
582
|
-
|
|
583
|
-
if "unit" not in self.data.columns:
|
|
584
|
-
raise DataException(
|
|
585
|
-
"Require a `unit` column to label unique units. This is used for plotting purposes" # noqa: E501
|
|
586
|
-
)
|
|
587
|
-
|
|
588
|
-
if _is_variable_dummy_coded(self.data[self.group_variable_name]) is False:
|
|
589
|
-
raise DataException(
|
|
590
|
-
f"""The grouping variable {self.group_variable_name} should be dummy
|
|
591
|
-
coded. Consisting of 0's and 1's only."""
|
|
592
|
-
)
|
|
593
|
-
|
|
594
558
|
def plot(self, round_to=None):
|
|
595
559
|
"""Plot the results.
|
|
596
560
|
|
|
@@ -745,11 +709,11 @@ class DifferenceInDifferences(ExperimentalDesign):
|
|
|
745
709
|
print(f"{self.expt_type:=^80}")
|
|
746
710
|
print(f"Formula: {self.formula}")
|
|
747
711
|
print("\nResults:")
|
|
748
|
-
print(
|
|
712
|
+
print(self._causal_impact_summary_stat(round_to))
|
|
749
713
|
self.print_coefficients(round_to)
|
|
750
714
|
|
|
751
715
|
|
|
752
|
-
class RegressionDiscontinuity(ExperimentalDesign):
|
|
716
|
+
class RegressionDiscontinuity(ExperimentalDesign, RDDataValidator):
|
|
753
717
|
"""
|
|
754
718
|
A class to analyse sharp regression discontinuity experiments.
|
|
755
719
|
|
|
@@ -876,18 +840,6 @@ class RegressionDiscontinuity(ExperimentalDesign):
|
|
|
876
840
|
- self.pred_discon["posterior_predictive"].sel(obs_ind=0)["mu"]
|
|
877
841
|
)
|
|
878
842
|
|
|
879
|
-
def _input_validation(self):
|
|
880
|
-
"""Validate the input data and model formula for correctness"""
|
|
881
|
-
if "treated" not in self.formula:
|
|
882
|
-
raise FormulaException(
|
|
883
|
-
"A predictor called `treated` should be in the formula"
|
|
884
|
-
)
|
|
885
|
-
|
|
886
|
-
if _is_variable_dummy_coded(self.data["treated"]) is False:
|
|
887
|
-
raise DataException(
|
|
888
|
-
"""The treated variable should be dummy coded. Consisting of 0's and 1's only.""" # noqa: E501
|
|
889
|
-
)
|
|
890
|
-
|
|
891
843
|
def _is_treated(self, x):
|
|
892
844
|
"""Returns ``True`` if `x` is greater than or equal to the treatment threshold.
|
|
893
845
|
|
|
@@ -951,7 +903,7 @@ class RegressionDiscontinuity(ExperimentalDesign):
|
|
|
951
903
|
)
|
|
952
904
|
return fig, ax
|
|
953
905
|
|
|
954
|
-
def summary(self, round_to
|
|
906
|
+
def summary(self, round_to=None) -> None:
|
|
955
907
|
"""
|
|
956
908
|
Print text output summarising the results
|
|
957
909
|
|
|
@@ -970,7 +922,7 @@ class RegressionDiscontinuity(ExperimentalDesign):
|
|
|
970
922
|
self.print_coefficients(round_to)
|
|
971
923
|
|
|
972
924
|
|
|
973
|
-
class RegressionKink(ExperimentalDesign):
|
|
925
|
+
class RegressionKink(ExperimentalDesign, RegressionKinkDataValidator):
|
|
974
926
|
"""
|
|
975
927
|
A class to analyse sharp regression kink experiments.
|
|
976
928
|
|
|
@@ -1095,24 +1047,6 @@ class RegressionKink(ExperimentalDesign):
|
|
|
1095
1047
|
mu_kink_right = predicted["posterior_predictive"].sel(obs_ind=2)["mu"]
|
|
1096
1048
|
return mu_kink_left, mu_kink, mu_kink_right
|
|
1097
1049
|
|
|
1098
|
-
def _input_validation(self):
|
|
1099
|
-
"""Validate the input data and model formula for correctness"""
|
|
1100
|
-
if "treated" not in self.formula:
|
|
1101
|
-
raise FormulaException(
|
|
1102
|
-
"A predictor called `treated` should be in the formula"
|
|
1103
|
-
)
|
|
1104
|
-
|
|
1105
|
-
if _is_variable_dummy_coded(self.data["treated"]) is False:
|
|
1106
|
-
raise DataException(
|
|
1107
|
-
"""The treated variable should be dummy coded. Consisting of 0's and 1's only.""" # noqa: E501
|
|
1108
|
-
)
|
|
1109
|
-
|
|
1110
|
-
if self.bandwidth <= 0:
|
|
1111
|
-
raise ValueError("The bandwidth must be greater than zero.")
|
|
1112
|
-
|
|
1113
|
-
if self.epsilon <= 0:
|
|
1114
|
-
raise ValueError("Epsilon must be greater than zero.")
|
|
1115
|
-
|
|
1116
1050
|
def _is_treated(self, x):
|
|
1117
1051
|
"""Returns ``True`` if `x` is greater than or equal to the treatment threshold.""" # noqa: E501
|
|
1118
1052
|
return np.greater_equal(x, self.kink_point)
|
|
@@ -1193,7 +1127,7 @@ class RegressionKink(ExperimentalDesign):
|
|
|
1193
1127
|
self.print_coefficients(round_to)
|
|
1194
1128
|
|
|
1195
1129
|
|
|
1196
|
-
class PrePostNEGD(ExperimentalDesign):
|
|
1130
|
+
class PrePostNEGD(ExperimentalDesign, PrePostNEGDDataValidator):
|
|
1197
1131
|
"""
|
|
1198
1132
|
A class to analyse data from pretest/posttest designs
|
|
1199
1133
|
|
|
@@ -1302,18 +1236,6 @@ class PrePostNEGD(ExperimentalDesign):
|
|
|
1302
1236
|
{"coeffs": self._get_treatment_effect_coeff()}
|
|
1303
1237
|
)
|
|
1304
1238
|
|
|
1305
|
-
# ================================================================
|
|
1306
|
-
|
|
1307
|
-
def _input_validation(self) -> None:
|
|
1308
|
-
"""Validate the input data and model formula for correctness"""
|
|
1309
|
-
if not _is_variable_dummy_coded(self.data[self.group_variable_name]):
|
|
1310
|
-
raise DataException(
|
|
1311
|
-
f"""
|
|
1312
|
-
There must be 2 levels of the grouping variable
|
|
1313
|
-
{self.group_variable_name}. I.e. the treated and untreated.
|
|
1314
|
-
"""
|
|
1315
|
-
)
|
|
1316
|
-
|
|
1317
1239
|
def plot(self, round_to=None):
|
|
1318
1240
|
"""Plot the results
|
|
1319
1241
|
|
|
@@ -1408,7 +1330,7 @@ class PrePostNEGD(ExperimentalDesign):
|
|
|
1408
1330
|
raise NameError("Unable to find coefficient name for the treatment effect")
|
|
1409
1331
|
|
|
1410
1332
|
|
|
1411
|
-
class InstrumentalVariable(ExperimentalDesign):
|
|
1333
|
+
class InstrumentalVariable(ExperimentalDesign, IVDataValidator):
|
|
1412
1334
|
"""
|
|
1413
1335
|
A class to analyse instrumental variable style experiments.
|
|
1414
1336
|
|
|
@@ -1555,26 +1477,3 @@ class InstrumentalVariable(ExperimentalDesign):
|
|
|
1555
1477
|
beta_params.insert(0, ols_reg.intercept_[0])
|
|
1556
1478
|
self.ols_beta_params = dict(zip(self._x_design_info.column_names, beta_params))
|
|
1557
1479
|
self.ols_reg = ols_reg
|
|
1558
|
-
|
|
1559
|
-
def _input_validation(self):
|
|
1560
|
-
"""Validate the input data and model formula for correctness"""
|
|
1561
|
-
treatment = self.instruments_formula.split("~")[0]
|
|
1562
|
-
test = treatment.strip() in self.instruments_data.columns
|
|
1563
|
-
test = test & (treatment.strip() in self.data.columns)
|
|
1564
|
-
if not test:
|
|
1565
|
-
raise DataException(
|
|
1566
|
-
f"""
|
|
1567
|
-
The treatment variable:
|
|
1568
|
-
{treatment} must appear in the instrument_data to be used
|
|
1569
|
-
as an outcome variable and in the data object to be used as a covariate.
|
|
1570
|
-
"""
|
|
1571
|
-
)
|
|
1572
|
-
Z = self.data[treatment.strip()]
|
|
1573
|
-
check_binary = len(np.unique(Z)) > 2
|
|
1574
|
-
if check_binary:
|
|
1575
|
-
warnings.warn(
|
|
1576
|
-
"""Warning. The treatment variable is not Binary.
|
|
1577
|
-
This is not necessarily a problem but it violates
|
|
1578
|
-
the assumption of a simple IV experiment.
|
|
1579
|
-
The coefficients should be interpreted appropriately."""
|
|
1580
|
-
)
|