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.
Files changed (42) hide show
  1. CausalPy-0.2.3/CONTRIBUTING.md +201 -0
  2. {CausalPy-0.2.1 → CausalPy-0.2.3}/CausalPy.egg-info/PKG-INFO +3 -3
  3. {CausalPy-0.2.1 → CausalPy-0.2.3}/CausalPy.egg-info/SOURCES.txt +9 -1
  4. {CausalPy-0.2.1 → CausalPy-0.2.3}/PKG-INFO +3 -3
  5. {CausalPy-0.2.1 → CausalPy-0.2.3}/README.md +1 -1
  6. CausalPy-0.2.3/causalpy/data_validation.py +135 -0
  7. {CausalPy-0.2.1 → CausalPy-0.2.3}/causalpy/pymc_experiments.py +18 -119
  8. {CausalPy-0.2.1 → CausalPy-0.2.3}/causalpy/pymc_models.py +4 -4
  9. {CausalPy-0.2.1 → CausalPy-0.2.3}/causalpy/skl_experiments.py +15 -4
  10. CausalPy-0.2.3/causalpy/tests/__init__.py +0 -0
  11. CausalPy-0.2.3/causalpy/tests/conftest.py +15 -0
  12. CausalPy-0.2.3/causalpy/tests/test_data_loading.py +32 -0
  13. CausalPy-0.2.3/causalpy/tests/test_integration_pymc_examples.py +499 -0
  14. CausalPy-0.2.3/causalpy/tests/test_integration_skl_examples.py +191 -0
  15. CausalPy-0.2.3/causalpy/tests/test_pymc_models.py +164 -0
  16. {CausalPy-0.2.1 → CausalPy-0.2.3}/causalpy/version.py +2 -1
  17. {CausalPy-0.2.1 → CausalPy-0.2.3}/pyproject.toml +3 -3
  18. {CausalPy-0.2.1 → CausalPy-0.2.3}/CausalPy.egg-info/dependency_links.txt +0 -0
  19. {CausalPy-0.2.1 → CausalPy-0.2.3}/CausalPy.egg-info/requires.txt +0 -0
  20. {CausalPy-0.2.1 → CausalPy-0.2.3}/CausalPy.egg-info/top_level.txt +0 -0
  21. {CausalPy-0.2.1 → CausalPy-0.2.3}/LICENSE +0 -0
  22. {CausalPy-0.2.1 → CausalPy-0.2.3}/causalpy/__init__.py +0 -0
  23. {CausalPy-0.2.1 → CausalPy-0.2.3}/causalpy/custom_exceptions.py +0 -0
  24. {CausalPy-0.2.1 → CausalPy-0.2.3}/causalpy/data/AJR2001.csv +0 -0
  25. {CausalPy-0.2.1 → CausalPy-0.2.3}/causalpy/data/GDP_in_dollars_billions.csv +0 -0
  26. {CausalPy-0.2.1 → CausalPy-0.2.3}/causalpy/data/__init__.py +0 -0
  27. {CausalPy-0.2.1 → CausalPy-0.2.3}/causalpy/data/ancova_generated.csv +0 -0
  28. {CausalPy-0.2.1 → CausalPy-0.2.3}/causalpy/data/banks.csv +0 -0
  29. {CausalPy-0.2.1 → CausalPy-0.2.3}/causalpy/data/datasets.py +0 -0
  30. {CausalPy-0.2.1 → CausalPy-0.2.3}/causalpy/data/deaths_and_temps_england_wales.csv +0 -0
  31. {CausalPy-0.2.1 → CausalPy-0.2.3}/causalpy/data/did.csv +0 -0
  32. {CausalPy-0.2.1 → CausalPy-0.2.3}/causalpy/data/drinking.csv +0 -0
  33. {CausalPy-0.2.1 → CausalPy-0.2.3}/causalpy/data/geolift1.csv +0 -0
  34. {CausalPy-0.2.1 → CausalPy-0.2.3}/causalpy/data/its.csv +0 -0
  35. {CausalPy-0.2.1 → CausalPy-0.2.3}/causalpy/data/its_simple.csv +0 -0
  36. {CausalPy-0.2.1 → CausalPy-0.2.3}/causalpy/data/regression_discontinuity.csv +0 -0
  37. {CausalPy-0.2.1 → CausalPy-0.2.3}/causalpy/data/simulate_data.py +0 -0
  38. {CausalPy-0.2.1 → CausalPy-0.2.3}/causalpy/data/synthetic_control.csv +0 -0
  39. {CausalPy-0.2.1 → CausalPy-0.2.3}/causalpy/plot_utils.py +0 -0
  40. {CausalPy-0.2.1 → CausalPy-0.2.3}/causalpy/skl_models.py +0 -0
  41. {CausalPy-0.2.1 → CausalPy-0.2.3}/causalpy/utils.py +0 -0
  42. {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
+ ![](docs/source/_static/classes.png)
189
+
190
+ Packages
191
+ ![](docs/source/_static/packages.png)
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.1
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.8
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
- | coming soon | ![](https://raw.githubusercontent.com/pymc-labs/CausalPy/main/docs/source/_static/interrupted_time_series_pymc.svg) |
433
+ | ![](https://raw.githubusercontent.com/pymc-labs/CausalPy/main/docs/source/_static/interrupted_time_series_skl.svg) | ![](https://raw.githubusercontent.com/pymc-labs/CausalPy/main/docs/source/_static/interrupted_time_series_pymc.svg) |
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.1
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.8
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
- | coming soon | ![](https://raw.githubusercontent.com/pymc-labs/CausalPy/main/docs/source/_static/interrupted_time_series_pymc.svg) |
433
+ | ![](https://raw.githubusercontent.com/pymc-labs/CausalPy/main/docs/source/_static/interrupted_time_series_skl.svg) | ![](https://raw.githubusercontent.com/pymc-labs/CausalPy/main/docs/source/_static/interrupted_time_series_pymc.svg) |
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
- | coming soon | ![](https://raw.githubusercontent.com/pymc-labs/CausalPy/main/docs/source/_static/interrupted_time_series_pymc.svg) |
181
+ | ![](https://raw.githubusercontent.com/pymc-labs/CausalPy/main/docs/source/_static/interrupted_time_series_skl.svg) | ![](https://raw.githubusercontent.com/pymc-labs/CausalPy/main/docs/source/_static/interrupted_time_series_pymc.svg) |
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.custom_exceptions import (
27
- BadIndexException, # NOQA
28
- DataException,
29
- FormulaException,
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 _is_variable_dummy_coded, round_num
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"].mu
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"].mu
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(round_num(self._causal_impact_summary_stat(), round_to))
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: None) -> None:
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
- )