formative-ds 1.2.0__tar.gz → 1.2.2__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.
- {formative_ds-1.2.0 → formative_ds-1.2.2}/PKG-INFO +4 -3
- {formative_ds-1.2.0 → formative_ds-1.2.2}/README.md +2 -2
- {formative_ds-1.2.0 → formative_ds-1.2.2}/docs/causal/estimators.rst +6 -0
- formative_ds-1.2.2/formative/causal/estimators/_base.py +93 -0
- {formative_ds-1.2.0 → formative_ds-1.2.2}/formative/causal/estimators/did.py +10 -57
- {formative_ds-1.2.0 → formative_ds-1.2.2}/formative/causal/estimators/iv.py +5 -61
- {formative_ds-1.2.0 → formative_ds-1.2.2}/formative/causal/estimators/matching.py +5 -35
- {formative_ds-1.2.0 → formative_ds-1.2.2}/formative/causal/estimators/ols.py +6 -62
- {formative_ds-1.2.0 → formative_ds-1.2.2}/formative/causal/estimators/rct.py +5 -61
- {formative_ds-1.2.0 → formative_ds-1.2.2}/formative/causal/estimators/rdd.py +5 -61
- {formative_ds-1.2.0 → formative_ds-1.2.2}/pyproject.toml +3 -2
- {formative_ds-1.2.0 → formative_ds-1.2.2}/uv.lock +16 -13
- {formative_ds-1.2.0 → formative_ds-1.2.2}/.github/workflows/ci.yml +0 -0
- {formative_ds-1.2.0 → formative_ds-1.2.2}/.github/workflows/publish.yml +0 -0
- {formative_ds-1.2.0 → formative_ds-1.2.2}/.gitignore +0 -0
- {formative_ds-1.2.0 → formative_ds-1.2.2}/CLAUDE.md +0 -0
- {formative_ds-1.2.0 → formative_ds-1.2.2}/LICENSE +0 -0
- {formative_ds-1.2.0 → formative_ds-1.2.2}/Makefile +0 -0
- {formative_ds-1.2.0 → formative_ds-1.2.2}/TODO.md +0 -0
- {formative_ds-1.2.0 → formative_ds-1.2.2}/docs/causal/dag.rst +0 -0
- {formative_ds-1.2.0 → formative_ds-1.2.2}/docs/causal/decisions.rst +0 -0
- {formative_ds-1.2.0 → formative_ds-1.2.2}/docs/causal/estimands.rst +0 -0
- {formative_ds-1.2.0 → formative_ds-1.2.2}/docs/causal/introduction.rst +0 -0
- {formative_ds-1.2.0 → formative_ds-1.2.2}/docs/causal/motivation.rst +0 -0
- {formative_ds-1.2.0 → formative_ds-1.2.2}/docs/causal/refutations.rst +0 -0
- {formative_ds-1.2.0 → formative_ds-1.2.2}/docs/causal/wizard.rst +0 -0
- {formative_ds-1.2.0 → formative_ds-1.2.2}/docs/conf.py +0 -0
- {formative_ds-1.2.0 → formative_ds-1.2.2}/docs/game/decision_rules.rst +0 -0
- {formative_ds-1.2.0 → formative_ds-1.2.2}/docs/game/introduction.rst +0 -0
- {formative_ds-1.2.0 → formative_ds-1.2.2}/docs/game/single_player.rst +0 -0
- {formative_ds-1.2.0 → formative_ds-1.2.2}/docs/index.rst +0 -0
- {formative_ds-1.2.0 → formative_ds-1.2.2}/examples/decision/01_decision_basic.py +0 -0
- {formative_ds-1.2.0 → formative_ds-1.2.2}/examples/did/01_did_basic.py +0 -0
- {formative_ds-1.2.0 → formative_ds-1.2.2}/examples/iv/01_iv_basic.py +0 -0
- {formative_ds-1.2.0 → formative_ds-1.2.2}/examples/iv/02_iv_vs_ols_bias.py +0 -0
- {formative_ds-1.2.0 → formative_ds-1.2.2}/examples/iv/03_iv_with_observed_confounder.py +0 -0
- {formative_ds-1.2.0 → formative_ds-1.2.2}/examples/iv/04_iv_refutation.py +0 -0
- {formative_ds-1.2.0 → formative_ds-1.2.2}/examples/matching/01_basic_matching.py +0 -0
- {formative_ds-1.2.0 → formative_ds-1.2.2}/examples/matching/02_matching_vs_naive.py +0 -0
- {formative_ds-1.2.0 → formative_ds-1.2.2}/examples/matching/03_matching_refutation.py +0 -0
- {formative_ds-1.2.0 → formative_ds-1.2.2}/examples/ols/01_ols_basic.py +0 -0
- {formative_ds-1.2.0 → formative_ds-1.2.2}/examples/ols/02_ols_unobserved_confounder.py +0 -0
- {formative_ds-1.2.0 → formative_ds-1.2.2}/examples/ols/03_ols_multiple_confounders.py +0 -0
- {formative_ds-1.2.0 → formative_ds-1.2.2}/examples/rct/01_rct_basic.py +0 -0
- {formative_ds-1.2.0 → formative_ds-1.2.2}/examples/rdd/01_rdd_basic.py +0 -0
- {formative_ds-1.2.0 → formative_ds-1.2.2}/examples/rdd/02_rdd_positive_running_var.py +0 -0
- {formative_ds-1.2.0 → formative_ds-1.2.2}/formative/__init__.py +0 -0
- {formative_ds-1.2.0 → formative_ds-1.2.2}/formative/causal/__init__.py +0 -0
- {formative_ds-1.2.0 → formative_ds-1.2.2}/formative/causal/_exceptions.py +0 -0
- {formative_ds-1.2.0 → formative_ds-1.2.2}/formative/causal/_explain.py +0 -0
- {formative_ds-1.2.0 → formative_ds-1.2.2}/formative/causal/dag.py +0 -0
- {formative_ds-1.2.0 → formative_ds-1.2.2}/formative/causal/decision.py +0 -0
- {formative_ds-1.2.0 → formative_ds-1.2.2}/formative/causal/estimators/__init__.py +0 -0
- {formative_ds-1.2.0 → formative_ds-1.2.2}/formative/causal/refutations/__init__.py +0 -0
- {formative_ds-1.2.0 → formative_ds-1.2.2}/formative/causal/refutations/_check.py +0 -0
- {formative_ds-1.2.0 → formative_ds-1.2.2}/formative/causal/refutations/did.py +0 -0
- {formative_ds-1.2.0 → formative_ds-1.2.2}/formative/causal/refutations/iv.py +0 -0
- {formative_ds-1.2.0 → formative_ds-1.2.2}/formative/causal/refutations/matching.py +0 -0
- {formative_ds-1.2.0 → formative_ds-1.2.2}/formative/causal/refutations/ols.py +0 -0
- {formative_ds-1.2.0 → formative_ds-1.2.2}/formative/causal/refutations/rct.py +0 -0
- {formative_ds-1.2.0 → formative_ds-1.2.2}/formative/causal/refutations/rdd.py +0 -0
- {formative_ds-1.2.0 → formative_ds-1.2.2}/formative/game/__init__.py +0 -0
- {formative_ds-1.2.0 → formative_ds-1.2.2}/formative/game/expected_value.py +0 -0
- {formative_ds-1.2.0 → formative_ds-1.2.2}/formative/game/hurwicz.py +0 -0
- {formative_ds-1.2.0 → formative_ds-1.2.2}/formative/game/laplace.py +0 -0
- {formative_ds-1.2.0 → formative_ds-1.2.2}/formative/game/maximax.py +0 -0
- {formative_ds-1.2.0 → formative_ds-1.2.2}/formative/game/maximin.py +0 -0
- {formative_ds-1.2.0 → formative_ds-1.2.2}/formative/game/minimax_regret.py +0 -0
- {formative_ds-1.2.0 → formative_ds-1.2.2}/tests/test_dag.py +0 -0
- {formative_ds-1.2.0 → formative_ds-1.2.2}/tests/test_decision.py +0 -0
- {formative_ds-1.2.0 → formative_ds-1.2.2}/tests/test_did.py +0 -0
- {formative_ds-1.2.0 → formative_ds-1.2.2}/tests/test_did_refutation.py +0 -0
- {formative_ds-1.2.0 → formative_ds-1.2.2}/tests/test_game.py +0 -0
- {formative_ds-1.2.0 → formative_ds-1.2.2}/tests/test_iv.py +0 -0
- {formative_ds-1.2.0 → formative_ds-1.2.2}/tests/test_iv_refutation.py +0 -0
- {formative_ds-1.2.0 → formative_ds-1.2.2}/tests/test_matching.py +0 -0
- {formative_ds-1.2.0 → formative_ds-1.2.2}/tests/test_matching_refutation.py +0 -0
- {formative_ds-1.2.0 → formative_ds-1.2.2}/tests/test_ols.py +0 -0
- {formative_ds-1.2.0 → formative_ds-1.2.2}/tests/test_ols_refutation.py +0 -0
- {formative_ds-1.2.0 → formative_ds-1.2.2}/tests/test_rct.py +0 -0
- {formative_ds-1.2.0 → formative_ds-1.2.2}/tests/test_rct_refutation.py +0 -0
- {formative_ds-1.2.0 → formative_ds-1.2.2}/tests/test_rdd.py +0 -0
- {formative_ds-1.2.0 → formative_ds-1.2.2}/tests/test_rdd_refutation.py +0 -0
- {formative_ds-1.2.0 → formative_ds-1.2.2}/vercel.json +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: formative-ds
|
|
3
|
-
Version: 1.2.
|
|
3
|
+
Version: 1.2.2
|
|
4
4
|
Summary: A Python library for quantitative reasoning.
|
|
5
5
|
Project-URL: Homepage, https://getformative.dev
|
|
6
6
|
Project-URL: Documentation, https://docs.getformative.dev
|
|
@@ -21,6 +21,7 @@ Classifier: Topic :: Scientific/Engineering :: Mathematics
|
|
|
21
21
|
Requires-Python: >=3.10
|
|
22
22
|
Requires-Dist: numpy<3,>=2
|
|
23
23
|
Requires-Dist: pandas<4,>=2.1
|
|
24
|
+
Requires-Dist: scipy<2,>=1.11
|
|
24
25
|
Requires-Dist: statsmodels<0.15,>=0.14
|
|
25
26
|
Description-Content-Type: text/markdown
|
|
26
27
|
|
|
@@ -47,7 +48,7 @@ Comprehensive documentation is available at [docs.getformative.dev](https://docs
|
|
|
47
48
|
### Causal estimation
|
|
48
49
|
|
|
49
50
|
```python
|
|
50
|
-
from formative import DAG, OLSObservational
|
|
51
|
+
from formative.causal import DAG, OLSObservational
|
|
51
52
|
|
|
52
53
|
dag = DAG()
|
|
53
54
|
dag.assume("ability").causes("education", "income")
|
|
@@ -119,7 +120,7 @@ To use `formative` from a script outside this repo without installing it, either
|
|
|
119
120
|
import sys
|
|
120
121
|
sys.path.insert(0, "/path/to/formative")
|
|
121
122
|
|
|
122
|
-
from formative import DAG, OLSObservational
|
|
123
|
+
from formative.causal import DAG, OLSObservational
|
|
123
124
|
```
|
|
124
125
|
|
|
125
126
|
Or set `PYTHONPATH` before running:
|
|
@@ -21,7 +21,7 @@ Comprehensive documentation is available at [docs.getformative.dev](https://docs
|
|
|
21
21
|
### Causal estimation
|
|
22
22
|
|
|
23
23
|
```python
|
|
24
|
-
from formative import DAG, OLSObservational
|
|
24
|
+
from formative.causal import DAG, OLSObservational
|
|
25
25
|
|
|
26
26
|
dag = DAG()
|
|
27
27
|
dag.assume("ability").causes("education", "income")
|
|
@@ -93,7 +93,7 @@ To use `formative` from a script outside this repo without installing it, either
|
|
|
93
93
|
import sys
|
|
94
94
|
sys.path.insert(0, "/path/to/formative")
|
|
95
95
|
|
|
96
|
-
from formative import DAG, OLSObservational
|
|
96
|
+
from formative.causal import DAG, OLSObservational
|
|
97
97
|
```
|
|
98
98
|
|
|
99
99
|
Or set `PYTHONPATH` before running:
|
|
@@ -12,6 +12,7 @@ OLSResult
|
|
|
12
12
|
|
|
13
13
|
.. autoclass:: formative.causal.OLSResult
|
|
14
14
|
:members:
|
|
15
|
+
:inherited-members:
|
|
15
16
|
|
|
16
17
|
IV2SLS
|
|
17
18
|
------
|
|
@@ -24,6 +25,7 @@ IVResult
|
|
|
24
25
|
|
|
25
26
|
.. autoclass:: formative.causal.IVResult
|
|
26
27
|
:members:
|
|
28
|
+
:inherited-members:
|
|
27
29
|
|
|
28
30
|
PropensityScoreMatching
|
|
29
31
|
-----------------------
|
|
@@ -36,6 +38,7 @@ MatchingResult
|
|
|
36
38
|
|
|
37
39
|
.. autoclass:: formative.causal.MatchingResult
|
|
38
40
|
:members:
|
|
41
|
+
:inherited-members:
|
|
39
42
|
|
|
40
43
|
RCT
|
|
41
44
|
---
|
|
@@ -48,6 +51,7 @@ RCTResult
|
|
|
48
51
|
|
|
49
52
|
.. autoclass:: formative.causal.RCTResult
|
|
50
53
|
:members:
|
|
54
|
+
:inherited-members:
|
|
51
55
|
|
|
52
56
|
DiD
|
|
53
57
|
---
|
|
@@ -60,6 +64,7 @@ DiDResult
|
|
|
60
64
|
|
|
61
65
|
.. autoclass:: formative.causal.DiDResult
|
|
62
66
|
:members:
|
|
67
|
+
:inherited-members:
|
|
63
68
|
|
|
64
69
|
RDD
|
|
65
70
|
---
|
|
@@ -72,3 +77,4 @@ RDDResult
|
|
|
72
77
|
|
|
73
78
|
.. autoclass:: formative.causal.RDDResult
|
|
74
79
|
:members:
|
|
80
|
+
:inherited-members:
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from ..refutations._check import Assumption
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class _BaseResult:
|
|
7
|
+
"""
|
|
8
|
+
Shared behaviour for estimation result classes.
|
|
9
|
+
|
|
10
|
+
Subclasses set ``self._treatment`` and ``self._outcome`` in ``__init__``
|
|
11
|
+
and define a class-level ``_ASSUMPTIONS`` list. ``decide()`` reads the
|
|
12
|
+
``effect`` / ``std_err`` / ``conf_int`` properties, which subclasses
|
|
13
|
+
(or ``_StatsmodelsResult``) provide.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
_ASSUMPTIONS: list[Assumption] = []
|
|
17
|
+
_treatment: str
|
|
18
|
+
_outcome: str
|
|
19
|
+
|
|
20
|
+
@property
|
|
21
|
+
def assumptions(self) -> list[Assumption]:
|
|
22
|
+
"""Modelling assumptions required for a causal interpretation."""
|
|
23
|
+
return list(self._ASSUMPTIONS)
|
|
24
|
+
|
|
25
|
+
def _assumptions_lines(self, width: int = 48) -> list[str]:
|
|
26
|
+
"""Assumptions footer appended to every ``summary()``."""
|
|
27
|
+
lines = ["", " Assumptions", " " + "┄" * width]
|
|
28
|
+
lines += [f" {a.fmt_tag()} {a.name}" for a in self._ASSUMPTIONS]
|
|
29
|
+
lines.append("")
|
|
30
|
+
return lines
|
|
31
|
+
|
|
32
|
+
def decide(self, cost: float, benefit: float):
|
|
33
|
+
"""
|
|
34
|
+
Compute a cost-benefit decision analysis from this causal estimate.
|
|
35
|
+
|
|
36
|
+
Parameters
|
|
37
|
+
----------
|
|
38
|
+
cost : float
|
|
39
|
+
Cost per unit of treatment applied.
|
|
40
|
+
benefit : float
|
|
41
|
+
Benefit (revenue, utility, etc.) per unit increase in the outcome.
|
|
42
|
+
|
|
43
|
+
Returns
|
|
44
|
+
-------
|
|
45
|
+
DecisionReport
|
|
46
|
+
Optimal decision, net benefit, CI, confidence, and robustness flag.
|
|
47
|
+
"""
|
|
48
|
+
from ..decision import _decide
|
|
49
|
+
|
|
50
|
+
return _decide(self.effect, self.std_err, self.conf_int, self._treatment, self._outcome, cost, benefit)
|
|
51
|
+
|
|
52
|
+
def __repr__(self) -> str:
|
|
53
|
+
return self.summary()
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class _StatsmodelsResult(_BaseResult):
|
|
57
|
+
"""
|
|
58
|
+
Result backed by a fitted statsmodels regression.
|
|
59
|
+
|
|
60
|
+
Effect statistics are read from ``self._result`` at parameter
|
|
61
|
+
``self._param``, which defaults to the treatment name (DiD overrides
|
|
62
|
+
it with the group × time interaction term).
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
def _param(self) -> str:
|
|
67
|
+
return self._treatment
|
|
68
|
+
|
|
69
|
+
@property
|
|
70
|
+
def effect(self) -> float:
|
|
71
|
+
"""Point estimate of the causal effect of treatment on outcome."""
|
|
72
|
+
return float(self._result.params[self._param])
|
|
73
|
+
|
|
74
|
+
@property
|
|
75
|
+
def std_err(self) -> float:
|
|
76
|
+
"""Standard error of the treatment effect estimate."""
|
|
77
|
+
return float(self._result.bse[self._param])
|
|
78
|
+
|
|
79
|
+
@property
|
|
80
|
+
def conf_int(self) -> tuple[float, float]:
|
|
81
|
+
"""95% confidence interval for the treatment effect."""
|
|
82
|
+
ci = self._result.conf_int()
|
|
83
|
+
return (float(ci.loc[self._param, 0]), float(ci.loc[self._param, 1]))
|
|
84
|
+
|
|
85
|
+
@property
|
|
86
|
+
def pvalue(self) -> float:
|
|
87
|
+
"""p-value for the treatment effect (``H0: effect = 0``)."""
|
|
88
|
+
return float(self._result.pvalues[self._param])
|
|
89
|
+
|
|
90
|
+
@property
|
|
91
|
+
def statsmodels_result(self):
|
|
92
|
+
"""The underlying statsmodels result, for full diagnostics."""
|
|
93
|
+
return self._result
|
|
@@ -5,6 +5,7 @@ import statsmodels.formula.api as smf
|
|
|
5
5
|
|
|
6
6
|
from ..dag import DAG
|
|
7
7
|
from ..refutations._check import Assumption
|
|
8
|
+
from ._base import _StatsmodelsResult
|
|
8
9
|
|
|
9
10
|
DID_ASSUMPTIONS: list[Assumption] = [
|
|
10
11
|
Assumption(
|
|
@@ -17,7 +18,7 @@ DID_ASSUMPTIONS: list[Assumption] = [
|
|
|
17
18
|
]
|
|
18
19
|
|
|
19
20
|
|
|
20
|
-
class DiDResult:
|
|
21
|
+
class DiDResult(_StatsmodelsResult):
|
|
21
22
|
"""
|
|
22
23
|
The result of a Difference-in-Differences estimation.
|
|
23
24
|
|
|
@@ -26,6 +27,8 @@ class DiDResult:
|
|
|
26
27
|
control group's trajectory.
|
|
27
28
|
"""
|
|
28
29
|
|
|
30
|
+
_ASSUMPTIONS = DID_ASSUMPTIONS
|
|
31
|
+
|
|
29
32
|
def __init__(
|
|
30
33
|
self,
|
|
31
34
|
result,
|
|
@@ -45,45 +48,20 @@ class DiDResult:
|
|
|
45
48
|
self._n = n
|
|
46
49
|
|
|
47
50
|
@property
|
|
48
|
-
def
|
|
51
|
+
def _param(self) -> str:
|
|
52
|
+
# The DiD estimate is the group × time interaction coefficient.
|
|
49
53
|
return f"{self._group}:{self._time}"
|
|
50
54
|
|
|
51
55
|
@property
|
|
52
|
-
def
|
|
53
|
-
|
|
54
|
-
return
|
|
56
|
+
def _treatment(self) -> str:
|
|
57
|
+
# The treatment label used by decide() is the group indicator.
|
|
58
|
+
return self._group
|
|
55
59
|
|
|
56
60
|
@property
|
|
57
61
|
def naive_diff(self) -> float:
|
|
58
62
|
"""Naive post-period difference: treated mean minus control mean in the post period."""
|
|
59
63
|
return self._naive_diff
|
|
60
64
|
|
|
61
|
-
@property
|
|
62
|
-
def std_err(self) -> float:
|
|
63
|
-
"""Standard error of the DiD estimate."""
|
|
64
|
-
return float(self._result.bse[self._interaction])
|
|
65
|
-
|
|
66
|
-
@property
|
|
67
|
-
def conf_int(self) -> tuple[float, float]:
|
|
68
|
-
"""95% confidence interval for the DiD estimate."""
|
|
69
|
-
ci = self._result.conf_int()
|
|
70
|
-
return (float(ci.loc[self._interaction, 0]), float(ci.loc[self._interaction, 1]))
|
|
71
|
-
|
|
72
|
-
@property
|
|
73
|
-
def pvalue(self) -> float:
|
|
74
|
-
"""p-value for the DiD estimate (``H0: ATT = 0``)."""
|
|
75
|
-
return float(self._result.pvalues[self._interaction])
|
|
76
|
-
|
|
77
|
-
@property
|
|
78
|
-
def statsmodels_result(self):
|
|
79
|
-
"""The underlying statsmodels OLS result, for full diagnostics."""
|
|
80
|
-
return self._result
|
|
81
|
-
|
|
82
|
-
@property
|
|
83
|
-
def assumptions(self) -> list[Assumption]:
|
|
84
|
-
"""Modelling assumptions required for a causal interpretation."""
|
|
85
|
-
return list(DID_ASSUMPTIONS)
|
|
86
|
-
|
|
87
65
|
def executive_summary(self) -> str:
|
|
88
66
|
"""Narrative explanation of the method, DAG, assumptions, and result."""
|
|
89
67
|
from .._explain import explain_did
|
|
@@ -107,35 +85,10 @@ class DiDResult:
|
|
|
107
85
|
f" 95% CI : [{lo:.4f}, {hi:.4f}]",
|
|
108
86
|
f" p-value : {self.pvalue:>10.4f}",
|
|
109
87
|
f" N : {self._n:>10}",
|
|
110
|
-
"",
|
|
111
|
-
" Assumptions",
|
|
112
|
-
" " + "\u2504" * 48,
|
|
113
88
|
]
|
|
114
|
-
|
|
115
|
-
lines.append(f" {a.fmt_tag()} {a.name}")
|
|
116
|
-
lines.append("")
|
|
89
|
+
lines += self._assumptions_lines()
|
|
117
90
|
return "\n".join(lines)
|
|
118
91
|
|
|
119
|
-
def decide(self, cost: float, benefit: float):
|
|
120
|
-
"""
|
|
121
|
-
Compute a cost-benefit decision analysis from this causal estimate.
|
|
122
|
-
|
|
123
|
-
Parameters
|
|
124
|
-
----------
|
|
125
|
-
cost : float
|
|
126
|
-
Cost per unit of treatment applied.
|
|
127
|
-
benefit : float
|
|
128
|
-
Benefit (revenue, utility, etc.) per unit increase in the outcome.
|
|
129
|
-
|
|
130
|
-
Returns
|
|
131
|
-
-------
|
|
132
|
-
DecisionReport
|
|
133
|
-
Optimal decision, net benefit, CI, confidence, and robustness flag.
|
|
134
|
-
"""
|
|
135
|
-
from ..decision import _decide
|
|
136
|
-
|
|
137
|
-
return _decide(self.effect, self.std_err, self.conf_int, self._group, self._outcome, cost, benefit)
|
|
138
|
-
|
|
139
92
|
def refute(self, data: pd.DataFrame):
|
|
140
93
|
"""
|
|
141
94
|
Run refutation checks against this DiD estimation.
|
|
@@ -7,6 +7,7 @@ from statsmodels.sandbox.regression.gmm import IV2SLS as _IV2SLS
|
|
|
7
7
|
|
|
8
8
|
from ..dag import DAG
|
|
9
9
|
from ..refutations._check import Assumption
|
|
10
|
+
from ._base import _StatsmodelsResult
|
|
10
11
|
|
|
11
12
|
IV_ASSUMPTIONS: list[Assumption] = [
|
|
12
13
|
Assumption("Relevance: the instrument strongly affects treatment", testable=True),
|
|
@@ -17,7 +18,7 @@ IV_ASSUMPTIONS: list[Assumption] = [
|
|
|
17
18
|
]
|
|
18
19
|
|
|
19
20
|
|
|
20
|
-
class IVResult:
|
|
21
|
+
class IVResult(_StatsmodelsResult):
|
|
21
22
|
"""
|
|
22
23
|
The result of an IV (2SLS) causal estimation.
|
|
23
24
|
|
|
@@ -26,6 +27,8 @@ class IVResult:
|
|
|
26
27
|
bias that IV corrects.
|
|
27
28
|
"""
|
|
28
29
|
|
|
30
|
+
_ASSUMPTIONS = IV_ASSUMPTIONS
|
|
31
|
+
|
|
29
32
|
def __init__(
|
|
30
33
|
self,
|
|
31
34
|
result,
|
|
@@ -46,78 +49,27 @@ class IVResult:
|
|
|
46
49
|
self._dag = dag
|
|
47
50
|
self._n = n
|
|
48
51
|
|
|
49
|
-
@property
|
|
50
|
-
def effect(self) -> float:
|
|
51
|
-
"""2SLS point estimate of the causal effect of treatment on outcome."""
|
|
52
|
-
return float(self._result.params[self._treatment])
|
|
53
|
-
|
|
54
52
|
@property
|
|
55
53
|
def unadjusted_effect(self) -> float:
|
|
56
54
|
"""Unadjusted OLS estimate: naive regression without instrument or controls."""
|
|
57
55
|
return float(self._unadjusted.params[self._treatment])
|
|
58
56
|
|
|
59
|
-
@property
|
|
60
|
-
def std_err(self) -> float:
|
|
61
|
-
"""Standard error of the 2SLS treatment effect estimate."""
|
|
62
|
-
return float(self._result.bse[self._treatment])
|
|
63
|
-
|
|
64
|
-
@property
|
|
65
|
-
def conf_int(self) -> tuple[float, float]:
|
|
66
|
-
"""95% confidence interval for the 2SLS treatment effect."""
|
|
67
|
-
ci = self._result.conf_int()
|
|
68
|
-
return (float(ci.loc[self._treatment, 0]), float(ci.loc[self._treatment, 1]))
|
|
69
|
-
|
|
70
|
-
@property
|
|
71
|
-
def pvalue(self) -> float:
|
|
72
|
-
"""p-value for the 2SLS treatment effect (``H0: effect = 0``)."""
|
|
73
|
-
return float(self._result.pvalues[self._treatment])
|
|
74
|
-
|
|
75
57
|
@property
|
|
76
58
|
def adjustment_set(self) -> set[str]:
|
|
77
59
|
"""Observed confounders included as controls in both OLS stages."""
|
|
78
60
|
return self._adjustment_set
|
|
79
61
|
|
|
80
|
-
@property
|
|
81
|
-
def statsmodels_result(self):
|
|
82
|
-
"""The underlying statsmodels IV2SLS result, for full diagnostics."""
|
|
83
|
-
return self._result
|
|
84
|
-
|
|
85
62
|
@property
|
|
86
63
|
def statsmodels_unadjusted_result(self):
|
|
87
64
|
"""The underlying unadjusted OLS result, for full diagnostics."""
|
|
88
65
|
return self._unadjusted
|
|
89
66
|
|
|
90
|
-
@property
|
|
91
|
-
def assumptions(self) -> list[Assumption]:
|
|
92
|
-
"""Modelling assumptions required for a causal interpretation."""
|
|
93
|
-
return list(IV_ASSUMPTIONS)
|
|
94
|
-
|
|
95
67
|
def executive_summary(self) -> str:
|
|
96
68
|
"""Narrative explanation of the method, DAG, assumptions, and result."""
|
|
97
69
|
from .._explain import explain_iv
|
|
98
70
|
|
|
99
71
|
return explain_iv(self)
|
|
100
72
|
|
|
101
|
-
def decide(self, cost: float, benefit: float):
|
|
102
|
-
"""
|
|
103
|
-
Compute a cost-benefit decision analysis from this causal estimate.
|
|
104
|
-
|
|
105
|
-
Parameters
|
|
106
|
-
----------
|
|
107
|
-
cost : float
|
|
108
|
-
Cost per unit of treatment applied.
|
|
109
|
-
benefit : float
|
|
110
|
-
Benefit (revenue, utility, etc.) per unit increase in the outcome.
|
|
111
|
-
|
|
112
|
-
Returns
|
|
113
|
-
-------
|
|
114
|
-
DecisionReport
|
|
115
|
-
Optimal decision, net benefit, CI, confidence, and robustness flag.
|
|
116
|
-
"""
|
|
117
|
-
from ..decision import _decide
|
|
118
|
-
|
|
119
|
-
return _decide(self.effect, self.std_err, self.conf_int, self._treatment, self._outcome, cost, benefit)
|
|
120
|
-
|
|
121
73
|
def refute(self, data: pd.DataFrame):
|
|
122
74
|
"""
|
|
123
75
|
Run refutation checks against this IV estimation.
|
|
@@ -181,18 +133,10 @@ class IVResult:
|
|
|
181
133
|
f" 95% CI : [{lo:.4f}, {hi:.4f}]",
|
|
182
134
|
f" p-value : {self.pvalue:>10.4f}",
|
|
183
135
|
f" N : {self._n:>10}",
|
|
184
|
-
"",
|
|
185
|
-
" Assumptions",
|
|
186
|
-
" " + "┄" * 48,
|
|
187
136
|
]
|
|
188
|
-
|
|
189
|
-
lines.append(f" {a.fmt_tag()} {a.name}")
|
|
190
|
-
lines.append("")
|
|
137
|
+
lines += self._assumptions_lines()
|
|
191
138
|
return "\n".join(lines)
|
|
192
139
|
|
|
193
|
-
def __repr__(self) -> str:
|
|
194
|
-
return self.summary()
|
|
195
|
-
|
|
196
140
|
|
|
197
141
|
class IV2SLS:
|
|
198
142
|
"""
|
|
@@ -7,6 +7,7 @@ import statsmodels.formula.api as smf
|
|
|
7
7
|
from .._exceptions import IdentificationError
|
|
8
8
|
from ..dag import DAG
|
|
9
9
|
from ..refutations._check import Assumption
|
|
10
|
+
from ._base import _BaseResult
|
|
10
11
|
|
|
11
12
|
MATCHING_ASSUMPTIONS: list[Assumption] = [
|
|
12
13
|
Assumption("Conditional independence: no unobserved confounders given matched variables", testable=False),
|
|
@@ -65,7 +66,7 @@ def _att_from_ps(
|
|
|
65
66
|
# ── Result ─────────────────────────────────────────────────────────────────────
|
|
66
67
|
|
|
67
68
|
|
|
68
|
-
class MatchingResult:
|
|
69
|
+
class MatchingResult(_BaseResult):
|
|
69
70
|
"""
|
|
70
71
|
The result of a propensity score matching estimation.
|
|
71
72
|
|
|
@@ -75,6 +76,8 @@ class MatchingResult:
|
|
|
75
76
|
full matching procedure.
|
|
76
77
|
"""
|
|
77
78
|
|
|
79
|
+
_ASSUMPTIONS = MATCHING_ASSUMPTIONS
|
|
80
|
+
|
|
78
81
|
def __init__(
|
|
79
82
|
self,
|
|
80
83
|
att: float,
|
|
@@ -136,11 +139,6 @@ class MatchingResult:
|
|
|
136
139
|
"""Full array of per-bootstrap ATT values, for diagnostics."""
|
|
137
140
|
return self._bootstrap_atts.copy()
|
|
138
141
|
|
|
139
|
-
@property
|
|
140
|
-
def assumptions(self) -> list[Assumption]:
|
|
141
|
-
"""Modelling assumptions required for a causal interpretation."""
|
|
142
|
-
return list(MATCHING_ASSUMPTIONS)
|
|
143
|
-
|
|
144
142
|
def executive_summary(self) -> str:
|
|
145
143
|
"""Narrative explanation of the method, DAG, assumptions, and result."""
|
|
146
144
|
from .._explain import explain_matching
|
|
@@ -179,35 +177,10 @@ class MatchingResult:
|
|
|
179
177
|
f" N : {self._n:>10}",
|
|
180
178
|
"",
|
|
181
179
|
" Matching: 1-to-1 nearest-neighbour on propensity score (with replacement)",
|
|
182
|
-
"",
|
|
183
|
-
" Assumptions",
|
|
184
|
-
" " + "┄" * 48,
|
|
185
180
|
]
|
|
186
|
-
|
|
187
|
-
lines.append(f" {a.fmt_tag()} {a.name}")
|
|
188
|
-
lines.append("")
|
|
181
|
+
lines += self._assumptions_lines()
|
|
189
182
|
return "\n".join(lines)
|
|
190
183
|
|
|
191
|
-
def decide(self, cost: float, benefit: float):
|
|
192
|
-
"""
|
|
193
|
-
Compute a cost-benefit decision analysis from this causal estimate.
|
|
194
|
-
|
|
195
|
-
Parameters
|
|
196
|
-
----------
|
|
197
|
-
cost : float
|
|
198
|
-
Cost per unit of treatment applied.
|
|
199
|
-
benefit : float
|
|
200
|
-
Benefit (revenue, utility, etc.) per unit increase in the outcome.
|
|
201
|
-
|
|
202
|
-
Returns
|
|
203
|
-
-------
|
|
204
|
-
DecisionReport
|
|
205
|
-
Optimal decision, net benefit, CI, confidence, and robustness flag.
|
|
206
|
-
"""
|
|
207
|
-
from ..decision import _decide
|
|
208
|
-
|
|
209
|
-
return _decide(self.effect, self.std_err, self.conf_int, self._treatment, self._outcome, cost, benefit)
|
|
210
|
-
|
|
211
184
|
def refute(self, data: pd.DataFrame):
|
|
212
185
|
"""
|
|
213
186
|
Run refutation checks against this matching estimation.
|
|
@@ -254,9 +227,6 @@ class MatchingResult:
|
|
|
254
227
|
outcome=self._outcome,
|
|
255
228
|
)
|
|
256
229
|
|
|
257
|
-
def __repr__(self) -> str:
|
|
258
|
-
return self.summary()
|
|
259
|
-
|
|
260
230
|
|
|
261
231
|
# ── Estimator ──────────────────────────────────────────────────────────────────
|
|
262
232
|
|
|
@@ -6,6 +6,7 @@ import statsmodels.formula.api as smf
|
|
|
6
6
|
from .._exceptions import IdentificationError
|
|
7
7
|
from ..dag import DAG
|
|
8
8
|
from ..refutations._check import Assumption
|
|
9
|
+
from ._base import _StatsmodelsResult
|
|
9
10
|
|
|
10
11
|
OLS_ASSUMPTIONS: list[Assumption] = [
|
|
11
12
|
Assumption("No unobserved confounders (selection on observables)", testable=False),
|
|
@@ -16,7 +17,7 @@ OLS_ASSUMPTIONS: list[Assumption] = [
|
|
|
16
17
|
]
|
|
17
18
|
|
|
18
19
|
|
|
19
|
-
class OLSResult:
|
|
20
|
+
class OLSResult(_StatsmodelsResult):
|
|
20
21
|
"""
|
|
21
22
|
The result of an OLS causal estimation.
|
|
22
23
|
|
|
@@ -25,6 +26,8 @@ class OLSResult:
|
|
|
25
26
|
effect of controlling for confounders directly.
|
|
26
27
|
"""
|
|
27
28
|
|
|
29
|
+
_ASSUMPTIONS = OLS_ASSUMPTIONS
|
|
30
|
+
|
|
28
31
|
def __init__(
|
|
29
32
|
self,
|
|
30
33
|
adjusted_result,
|
|
@@ -35,7 +38,7 @@ class OLSResult:
|
|
|
35
38
|
dag,
|
|
36
39
|
n: int,
|
|
37
40
|
) -> None:
|
|
38
|
-
self.
|
|
41
|
+
self._result = adjusted_result
|
|
39
42
|
self._unadjusted = unadjusted_result
|
|
40
43
|
self._treatment = treatment
|
|
41
44
|
self._outcome = outcome
|
|
@@ -43,52 +46,21 @@ class OLSResult:
|
|
|
43
46
|
self._dag = dag
|
|
44
47
|
self._n = n
|
|
45
48
|
|
|
46
|
-
@property
|
|
47
|
-
def effect(self) -> float:
|
|
48
|
-
"""Adjusted point estimate: causal effect of treatment on outcome."""
|
|
49
|
-
return float(self._adjusted.params[self._treatment])
|
|
50
|
-
|
|
51
49
|
@property
|
|
52
50
|
def unadjusted_effect(self) -> float:
|
|
53
51
|
"""Unadjusted point estimate: naive regression without controlling for confounders."""
|
|
54
52
|
return float(self._unadjusted.params[self._treatment])
|
|
55
53
|
|
|
56
|
-
@property
|
|
57
|
-
def std_err(self) -> float:
|
|
58
|
-
"""Standard error of the adjusted treatment effect estimate."""
|
|
59
|
-
return float(self._adjusted.bse[self._treatment])
|
|
60
|
-
|
|
61
|
-
@property
|
|
62
|
-
def conf_int(self) -> tuple[float, float]:
|
|
63
|
-
"""95% confidence interval for the adjusted treatment effect."""
|
|
64
|
-
ci = self._adjusted.conf_int()
|
|
65
|
-
return (float(ci.loc[self._treatment, 0]), float(ci.loc[self._treatment, 1]))
|
|
66
|
-
|
|
67
|
-
@property
|
|
68
|
-
def pvalue(self) -> float:
|
|
69
|
-
"""p-value for the adjusted treatment effect (``H0: effect = 0``)."""
|
|
70
|
-
return float(self._adjusted.pvalues[self._treatment])
|
|
71
|
-
|
|
72
54
|
@property
|
|
73
55
|
def adjustment_set(self) -> set[str]:
|
|
74
56
|
"""Variables controlled for to satisfy the backdoor criterion."""
|
|
75
57
|
return self._adjustment_set
|
|
76
58
|
|
|
77
|
-
@property
|
|
78
|
-
def statsmodels_result(self):
|
|
79
|
-
"""The underlying adjusted statsmodels result, for full diagnostics."""
|
|
80
|
-
return self._adjusted
|
|
81
|
-
|
|
82
59
|
@property
|
|
83
60
|
def statsmodels_unadjusted_result(self):
|
|
84
61
|
"""The underlying unadjusted statsmodels result, for full diagnostics."""
|
|
85
62
|
return self._unadjusted
|
|
86
63
|
|
|
87
|
-
@property
|
|
88
|
-
def assumptions(self) -> list[Assumption]:
|
|
89
|
-
"""Modelling assumptions required for a causal interpretation."""
|
|
90
|
-
return list(OLS_ASSUMPTIONS)
|
|
91
|
-
|
|
92
64
|
def executive_summary(self) -> str:
|
|
93
65
|
"""Narrative explanation of the method, DAG, assumptions, and result."""
|
|
94
66
|
from .._explain import explain_ols
|
|
@@ -124,35 +96,10 @@ class OLSResult:
|
|
|
124
96
|
f" 95% CI : [{lo:.4f}, {hi:.4f}]",
|
|
125
97
|
f" p-value : {self.pvalue:>10.4f}",
|
|
126
98
|
f" N : {self._n:>10}",
|
|
127
|
-
"",
|
|
128
|
-
" Assumptions",
|
|
129
|
-
" " + "┄" * 48,
|
|
130
99
|
]
|
|
131
|
-
|
|
132
|
-
lines.append(f" {a.fmt_tag()} {a.name}")
|
|
133
|
-
lines.append("")
|
|
100
|
+
lines += self._assumptions_lines()
|
|
134
101
|
return "\n".join(lines)
|
|
135
102
|
|
|
136
|
-
def decide(self, cost: float, benefit: float):
|
|
137
|
-
"""
|
|
138
|
-
Compute a cost-benefit decision analysis from this causal estimate.
|
|
139
|
-
|
|
140
|
-
Parameters
|
|
141
|
-
----------
|
|
142
|
-
cost : float
|
|
143
|
-
Cost per unit of treatment applied.
|
|
144
|
-
benefit : float
|
|
145
|
-
Benefit (revenue, utility, etc.) per unit increase in the outcome.
|
|
146
|
-
|
|
147
|
-
Returns
|
|
148
|
-
-------
|
|
149
|
-
DecisionReport
|
|
150
|
-
Optimal decision, net benefit, CI, confidence, and robustness flag.
|
|
151
|
-
"""
|
|
152
|
-
from ..decision import _decide
|
|
153
|
-
|
|
154
|
-
return _decide(self.effect, self.std_err, self.conf_int, self._treatment, self._outcome, cost, benefit)
|
|
155
|
-
|
|
156
103
|
def refute(self, data: pd.DataFrame):
|
|
157
104
|
"""
|
|
158
105
|
Run refutation checks against this OLS estimation.
|
|
@@ -186,9 +133,6 @@ class OLSResult:
|
|
|
186
133
|
outcome=self._outcome,
|
|
187
134
|
)
|
|
188
135
|
|
|
189
|
-
def __repr__(self) -> str:
|
|
190
|
-
return self.summary()
|
|
191
|
-
|
|
192
136
|
|
|
193
137
|
class OLSObservational:
|
|
194
138
|
"""
|
|
@@ -5,6 +5,7 @@ import statsmodels.formula.api as smf
|
|
|
5
5
|
|
|
6
6
|
from ..dag import DAG
|
|
7
7
|
from ..refutations._check import Assumption
|
|
8
|
+
from ._base import _StatsmodelsResult
|
|
8
9
|
|
|
9
10
|
RCT_ASSUMPTIONS: list[Assumption] = [
|
|
10
11
|
Assumption("Random assignment of treatment", testable=False),
|
|
@@ -13,7 +14,7 @@ RCT_ASSUMPTIONS: list[Assumption] = [
|
|
|
13
14
|
]
|
|
14
15
|
|
|
15
16
|
|
|
16
|
-
class RCTResult:
|
|
17
|
+
class RCTResult(_StatsmodelsResult):
|
|
17
18
|
"""
|
|
18
19
|
The result of an RCT causal estimation.
|
|
19
20
|
|
|
@@ -22,6 +23,8 @@ class RCTResult:
|
|
|
22
23
|
equals the difference in mean outcomes between treatment and control.
|
|
23
24
|
"""
|
|
24
25
|
|
|
26
|
+
_ASSUMPTIONS = RCT_ASSUMPTIONS
|
|
27
|
+
|
|
25
28
|
def __init__(
|
|
26
29
|
self,
|
|
27
30
|
result,
|
|
@@ -36,37 +39,6 @@ class RCTResult:
|
|
|
36
39
|
self._dag = dag
|
|
37
40
|
self._n = n
|
|
38
41
|
|
|
39
|
-
@property
|
|
40
|
-
def effect(self) -> float:
|
|
41
|
-
"""ATE: average treatment effect (difference in means)."""
|
|
42
|
-
return float(self._result.params[self._treatment])
|
|
43
|
-
|
|
44
|
-
@property
|
|
45
|
-
def std_err(self) -> float:
|
|
46
|
-
"""Standard error of the ATE estimate."""
|
|
47
|
-
return float(self._result.bse[self._treatment])
|
|
48
|
-
|
|
49
|
-
@property
|
|
50
|
-
def conf_int(self) -> tuple[float, float]:
|
|
51
|
-
"""95% confidence interval for the ATE."""
|
|
52
|
-
ci = self._result.conf_int()
|
|
53
|
-
return (float(ci.loc[self._treatment, 0]), float(ci.loc[self._treatment, 1]))
|
|
54
|
-
|
|
55
|
-
@property
|
|
56
|
-
def pvalue(self) -> float:
|
|
57
|
-
"""p-value for the ATE (``H0: ATE = 0``)."""
|
|
58
|
-
return float(self._result.pvalues[self._treatment])
|
|
59
|
-
|
|
60
|
-
@property
|
|
61
|
-
def statsmodels_result(self):
|
|
62
|
-
"""The underlying statsmodels OLS result, for full diagnostics."""
|
|
63
|
-
return self._result
|
|
64
|
-
|
|
65
|
-
@property
|
|
66
|
-
def assumptions(self) -> list[Assumption]:
|
|
67
|
-
"""Modelling assumptions required for a causal interpretation."""
|
|
68
|
-
return list(RCT_ASSUMPTIONS)
|
|
69
|
-
|
|
70
42
|
def executive_summary(self) -> str:
|
|
71
43
|
"""Narrative explanation of the method, DAG, assumptions, and result."""
|
|
72
44
|
from .._explain import explain_rct
|
|
@@ -87,35 +59,10 @@ class RCTResult:
|
|
|
87
59
|
f" 95% CI : [{lo:.4f}, {hi:.4f}]",
|
|
88
60
|
f" p-value : {self.pvalue:>10.4f}",
|
|
89
61
|
f" N : {self._n:>10}",
|
|
90
|
-
"",
|
|
91
|
-
" Assumptions",
|
|
92
|
-
" " + "┄" * 48,
|
|
93
62
|
]
|
|
94
|
-
|
|
95
|
-
lines.append(f" {a.fmt_tag()} {a.name}")
|
|
96
|
-
lines.append("")
|
|
63
|
+
lines += self._assumptions_lines()
|
|
97
64
|
return "\n".join(lines)
|
|
98
65
|
|
|
99
|
-
def decide(self, cost: float, benefit: float):
|
|
100
|
-
"""
|
|
101
|
-
Compute a cost-benefit decision analysis from this causal estimate.
|
|
102
|
-
|
|
103
|
-
Parameters
|
|
104
|
-
----------
|
|
105
|
-
cost : float
|
|
106
|
-
Cost per unit of treatment applied.
|
|
107
|
-
benefit : float
|
|
108
|
-
Benefit (revenue, utility, etc.) per unit increase in the outcome.
|
|
109
|
-
|
|
110
|
-
Returns
|
|
111
|
-
-------
|
|
112
|
-
DecisionReport
|
|
113
|
-
Optimal decision, net benefit, CI, confidence, and robustness flag.
|
|
114
|
-
"""
|
|
115
|
-
from ..decision import _decide
|
|
116
|
-
|
|
117
|
-
return _decide(self.effect, self.std_err, self.conf_int, self._treatment, self._outcome, cost, benefit)
|
|
118
|
-
|
|
119
66
|
def refute(self, data: pd.DataFrame):
|
|
120
67
|
"""
|
|
121
68
|
Run refutation checks against this RCT estimation.
|
|
@@ -149,9 +96,6 @@ class RCTResult:
|
|
|
149
96
|
outcome=self._outcome,
|
|
150
97
|
)
|
|
151
98
|
|
|
152
|
-
def __repr__(self) -> str:
|
|
153
|
-
return self.summary()
|
|
154
|
-
|
|
155
99
|
|
|
156
100
|
class RCT:
|
|
157
101
|
"""
|
|
@@ -5,6 +5,7 @@ import statsmodels.formula.api as smf
|
|
|
5
5
|
|
|
6
6
|
from ..dag import DAG
|
|
7
7
|
from ..refutations._check import Assumption
|
|
8
|
+
from ._base import _StatsmodelsResult
|
|
8
9
|
|
|
9
10
|
RDD_ASSUMPTIONS: list[Assumption] = [
|
|
10
11
|
Assumption(
|
|
@@ -23,7 +24,7 @@ RDD_ASSUMPTIONS: list[Assumption] = [
|
|
|
23
24
|
]
|
|
24
25
|
|
|
25
26
|
|
|
26
|
-
class RDDResult:
|
|
27
|
+
class RDDResult(_StatsmodelsResult):
|
|
27
28
|
"""
|
|
28
29
|
The result of a Regression Discontinuity Design estimation.
|
|
29
30
|
|
|
@@ -31,6 +32,8 @@ class RDDResult:
|
|
|
31
32
|
the cutoff, estimated via local linear regression on both sides of the threshold.
|
|
32
33
|
"""
|
|
33
34
|
|
|
35
|
+
_ASSUMPTIONS = RDD_ASSUMPTIONS
|
|
36
|
+
|
|
34
37
|
def __init__(
|
|
35
38
|
self,
|
|
36
39
|
result,
|
|
@@ -51,42 +54,11 @@ class RDDResult:
|
|
|
51
54
|
self._unadjusted_effect = unadjusted_effect
|
|
52
55
|
self._dag = dag
|
|
53
56
|
|
|
54
|
-
@property
|
|
55
|
-
def effect(self) -> float:
|
|
56
|
-
"""LATE at the cutoff: the jump in outcome at the threshold (coefficient on the treatment indicator)."""
|
|
57
|
-
return float(self._result.params[self._treatment])
|
|
58
|
-
|
|
59
57
|
@property
|
|
60
58
|
def unadjusted_effect(self) -> float:
|
|
61
59
|
"""Naive mean difference: above-cutoff mean minus below-cutoff mean."""
|
|
62
60
|
return self._unadjusted_effect
|
|
63
61
|
|
|
64
|
-
@property
|
|
65
|
-
def std_err(self) -> float:
|
|
66
|
-
"""Standard error of the LATE at the cutoff estimate."""
|
|
67
|
-
return float(self._result.bse[self._treatment])
|
|
68
|
-
|
|
69
|
-
@property
|
|
70
|
-
def conf_int(self) -> tuple[float, float]:
|
|
71
|
-
"""95% confidence interval for the LATE at the cutoff estimate."""
|
|
72
|
-
ci = self._result.conf_int()
|
|
73
|
-
return (float(ci.loc[self._treatment, 0]), float(ci.loc[self._treatment, 1]))
|
|
74
|
-
|
|
75
|
-
@property
|
|
76
|
-
def pvalue(self) -> float:
|
|
77
|
-
"""p-value for the LATE at the cutoff (``H0: LATE at cutoff = 0``)."""
|
|
78
|
-
return float(self._result.pvalues[self._treatment])
|
|
79
|
-
|
|
80
|
-
@property
|
|
81
|
-
def statsmodels_result(self):
|
|
82
|
-
"""The underlying statsmodels OLS result, for full diagnostics."""
|
|
83
|
-
return self._result
|
|
84
|
-
|
|
85
|
-
@property
|
|
86
|
-
def assumptions(self) -> list[Assumption]:
|
|
87
|
-
"""Modelling assumptions required for a causal interpretation."""
|
|
88
|
-
return list(RDD_ASSUMPTIONS)
|
|
89
|
-
|
|
90
62
|
@property
|
|
91
63
|
def cutoff(self) -> float:
|
|
92
64
|
"""The threshold value that determines treatment assignment."""
|
|
@@ -131,35 +103,10 @@ class RDDResult:
|
|
|
131
103
|
f" 95% CI : [{lo:.4f}, {hi:.4f}]",
|
|
132
104
|
f" p-value : {self.pvalue:>10.4f}",
|
|
133
105
|
f" N (in bandwidth) : {self.n_obs:>9}",
|
|
134
|
-
"",
|
|
135
|
-
" Assumptions",
|
|
136
|
-
" " + "\u2504" * 30,
|
|
137
106
|
]
|
|
138
|
-
|
|
139
|
-
lines.append(f" {a.fmt_tag()} {a.name}")
|
|
140
|
-
lines.append("")
|
|
107
|
+
lines += self._assumptions_lines(width=30)
|
|
141
108
|
return "\n".join(lines)
|
|
142
109
|
|
|
143
|
-
def decide(self, cost: float, benefit: float):
|
|
144
|
-
"""
|
|
145
|
-
Compute a cost-benefit decision analysis from this causal estimate.
|
|
146
|
-
|
|
147
|
-
Parameters
|
|
148
|
-
----------
|
|
149
|
-
cost : float
|
|
150
|
-
Cost per unit of treatment applied.
|
|
151
|
-
benefit : float
|
|
152
|
-
Benefit (revenue, utility, etc.) per unit increase in the outcome.
|
|
153
|
-
|
|
154
|
-
Returns
|
|
155
|
-
-------
|
|
156
|
-
DecisionReport
|
|
157
|
-
Optimal decision, net benefit, CI, confidence, and robustness flag.
|
|
158
|
-
"""
|
|
159
|
-
from ..decision import _decide
|
|
160
|
-
|
|
161
|
-
return _decide(self.effect, self.std_err, self.conf_int, self._treatment, self._outcome, cost, benefit)
|
|
162
|
-
|
|
163
110
|
def refute(self, data: pd.DataFrame):
|
|
164
111
|
"""
|
|
165
112
|
Run refutation checks against this RDD estimation.
|
|
@@ -210,9 +157,6 @@ class RDDResult:
|
|
|
210
157
|
outcome=self._outcome,
|
|
211
158
|
)
|
|
212
159
|
|
|
213
|
-
def __repr__(self) -> str:
|
|
214
|
-
return self.summary()
|
|
215
|
-
|
|
216
160
|
|
|
217
161
|
class RDD:
|
|
218
162
|
"""
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "formative-ds"
|
|
7
|
-
version = "1.2.
|
|
7
|
+
version = "1.2.2"
|
|
8
8
|
description = "A Python library for quantitative reasoning."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -25,6 +25,7 @@ classifiers = [
|
|
|
25
25
|
|
|
26
26
|
dependencies = [
|
|
27
27
|
"numpy>=2,<3",
|
|
28
|
+
"scipy>=1.11,<2",
|
|
28
29
|
"statsmodels>=0.14,<0.15",
|
|
29
30
|
"pandas>=2.1,<4",
|
|
30
31
|
]
|
|
@@ -42,7 +43,7 @@ docs = ["sphinx>=7,<10", "sphinx-rtd-theme>=2,<4", "sphinx-copybutton>=0.5,<0.6"
|
|
|
42
43
|
packages = ["formative"]
|
|
43
44
|
|
|
44
45
|
[tool.bumpversion]
|
|
45
|
-
current_version = "1.2.
|
|
46
|
+
current_version = "1.2.2"
|
|
46
47
|
commit = true
|
|
47
48
|
tag = true
|
|
48
49
|
tag_name = "v{new_version}"
|
|
@@ -303,13 +303,15 @@ wheels = [
|
|
|
303
303
|
|
|
304
304
|
[[package]]
|
|
305
305
|
name = "formative-ds"
|
|
306
|
-
version = "1.
|
|
306
|
+
version = "1.2.1"
|
|
307
307
|
source = { editable = "." }
|
|
308
308
|
dependencies = [
|
|
309
309
|
{ name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
|
|
310
310
|
{ name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
|
|
311
311
|
{ name = "pandas", version = "2.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
|
|
312
312
|
{ name = "pandas", version = "3.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
|
|
313
|
+
{ name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
|
|
314
|
+
{ name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
|
|
313
315
|
{ name = "statsmodels" },
|
|
314
316
|
]
|
|
315
317
|
|
|
@@ -334,6 +336,7 @@ docs = [
|
|
|
334
336
|
requires-dist = [
|
|
335
337
|
{ name = "numpy", specifier = ">=2,<3" },
|
|
336
338
|
{ name = "pandas", specifier = ">=2.1,<4" },
|
|
339
|
+
{ name = "scipy", specifier = ">=1.11,<2" },
|
|
337
340
|
{ name = "statsmodels", specifier = ">=0.14,<0.15" },
|
|
338
341
|
]
|
|
339
342
|
|
|
@@ -352,11 +355,11 @@ docs = [
|
|
|
352
355
|
|
|
353
356
|
[[package]]
|
|
354
357
|
name = "idna"
|
|
355
|
-
version = "3.
|
|
358
|
+
version = "3.15"
|
|
356
359
|
source = { registry = "https://pypi.org/simple" }
|
|
357
|
-
sdist = { url = "https://files.pythonhosted.org/packages/
|
|
360
|
+
sdist = { url = "https://files.pythonhosted.org/packages/82/77/7b3966d0b9d1d31a36ddf1746926a11dface89a83409bf1483f0237aa758/idna-3.15.tar.gz", hash = "sha256:ca962446ea538f7092a95e057da437618e886f4d349216d2b1e294abfdb65fdc", size = 199245, upload-time = "2026-05-12T22:45:57.011Z" }
|
|
358
361
|
wheels = [
|
|
359
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
362
|
+
{ url = "https://files.pythonhosted.org/packages/d2/23/408243171aa9aaba178d3e2559159c24c1171a641aa83b67bdd3394ead8e/idna-3.15-py3-none-any.whl", hash = "sha256:048adeaf8c2d788c40fee287673ccaa74c24ffd8dcf09ffa555a2fbb59f10ac8", size = 72340, upload-time = "2026-05-12T22:45:55.733Z" },
|
|
360
363
|
]
|
|
361
364
|
|
|
362
365
|
[[package]]
|
|
@@ -797,16 +800,16 @@ wheels = [
|
|
|
797
800
|
|
|
798
801
|
[[package]]
|
|
799
802
|
name = "pygments"
|
|
800
|
-
version = "2.
|
|
803
|
+
version = "2.20.0"
|
|
801
804
|
source = { registry = "https://pypi.org/simple" }
|
|
802
|
-
sdist = { url = "https://files.pythonhosted.org/packages/
|
|
805
|
+
sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" }
|
|
803
806
|
wheels = [
|
|
804
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
807
|
+
{ url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" },
|
|
805
808
|
]
|
|
806
809
|
|
|
807
810
|
[[package]]
|
|
808
811
|
name = "pytest"
|
|
809
|
-
version = "9.0.
|
|
812
|
+
version = "9.0.3"
|
|
810
813
|
source = { registry = "https://pypi.org/simple" }
|
|
811
814
|
dependencies = [
|
|
812
815
|
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
|
@@ -817,9 +820,9 @@ dependencies = [
|
|
|
817
820
|
{ name = "pygments" },
|
|
818
821
|
{ name = "tomli", marker = "python_full_version < '3.11'" },
|
|
819
822
|
]
|
|
820
|
-
sdist = { url = "https://files.pythonhosted.org/packages/
|
|
823
|
+
sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" }
|
|
821
824
|
wheels = [
|
|
822
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
825
|
+
{ url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" },
|
|
823
826
|
]
|
|
824
827
|
|
|
825
828
|
[[package]]
|
|
@@ -1438,9 +1441,9 @@ wheels = [
|
|
|
1438
1441
|
|
|
1439
1442
|
[[package]]
|
|
1440
1443
|
name = "urllib3"
|
|
1441
|
-
version = "2.
|
|
1444
|
+
version = "2.7.0"
|
|
1442
1445
|
source = { registry = "https://pypi.org/simple" }
|
|
1443
|
-
sdist = { url = "https://files.pythonhosted.org/packages/
|
|
1446
|
+
sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" }
|
|
1444
1447
|
wheels = [
|
|
1445
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
1448
|
+
{ url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" },
|
|
1446
1449
|
]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|