formative-ds 1.2.2__tar.gz → 2.0.0__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.2 → formative_ds-2.0.0}/CLAUDE.md +11 -3
- {formative_ds-1.2.2 → formative_ds-2.0.0}/PKG-INFO +3 -3
- {formative_ds-1.2.2 → formative_ds-2.0.0}/README.md +2 -2
- {formative_ds-1.2.2 → formative_ds-2.0.0}/docs/causal/decisions.rst +3 -24
- {formative_ds-1.2.2 → formative_ds-2.0.0}/docs/causal/estimands.rst +49 -1
- {formative_ds-1.2.2 → formative_ds-2.0.0}/docs/causal/estimators.rst +20 -0
- {formative_ds-1.2.2 → formative_ds-2.0.0}/docs/game/decision_rules.rst +7 -7
- {formative_ds-1.2.2 → formative_ds-2.0.0}/formative/causal/__init__.py +6 -2
- {formative_ds-1.2.2 → formative_ds-2.0.0}/formative/causal/_explain.py +44 -0
- {formative_ds-1.2.2 → formative_ds-2.0.0}/formative/causal/decision.py +1 -46
- {formative_ds-1.2.2 → formative_ds-2.0.0}/formative/causal/estimators/_base.py +4 -0
- formative_ds-2.0.0/formative/causal/estimators/_cate.py +282 -0
- {formative_ds-1.2.2 → formative_ds-2.0.0}/formative/causal/estimators/ols.py +116 -2
- {formative_ds-1.2.2 → formative_ds-2.0.0}/formative/causal/estimators/rct.py +104 -1
- formative_ds-2.0.0/formative/causal/refutations/cate.py +141 -0
- {formative_ds-1.2.2 → formative_ds-2.0.0}/formative/game/__init__.py +4 -4
- formative_ds-1.2.2/formative/game/minimax_regret.py → formative_ds-2.0.0/formative/game/minimax.py +9 -9
- {formative_ds-1.2.2 → formative_ds-2.0.0}/pyproject.toml +2 -2
- formative_ds-2.0.0/tests/test_cate.py +240 -0
- formative_ds-2.0.0/tests/test_cate_refutation.py +109 -0
- {formative_ds-1.2.2 → formative_ds-2.0.0}/tests/test_decision.py +0 -26
- {formative_ds-1.2.2 → formative_ds-2.0.0}/tests/test_game.py +10 -10
- {formative_ds-1.2.2 → formative_ds-2.0.0}/uv.lock +1 -1
- formative_ds-1.2.2/TODO.md +0 -51
- {formative_ds-1.2.2 → formative_ds-2.0.0}/.github/workflows/ci.yml +0 -0
- {formative_ds-1.2.2 → formative_ds-2.0.0}/.github/workflows/publish.yml +0 -0
- {formative_ds-1.2.2 → formative_ds-2.0.0}/.gitignore +0 -0
- {formative_ds-1.2.2 → formative_ds-2.0.0}/LICENSE +0 -0
- {formative_ds-1.2.2 → formative_ds-2.0.0}/Makefile +0 -0
- {formative_ds-1.2.2 → formative_ds-2.0.0}/docs/causal/dag.rst +0 -0
- {formative_ds-1.2.2 → formative_ds-2.0.0}/docs/causal/introduction.rst +0 -0
- {formative_ds-1.2.2 → formative_ds-2.0.0}/docs/causal/motivation.rst +0 -0
- {formative_ds-1.2.2 → formative_ds-2.0.0}/docs/causal/refutations.rst +0 -0
- {formative_ds-1.2.2 → formative_ds-2.0.0}/docs/causal/wizard.rst +0 -0
- {formative_ds-1.2.2 → formative_ds-2.0.0}/docs/conf.py +0 -0
- {formative_ds-1.2.2 → formative_ds-2.0.0}/docs/game/introduction.rst +0 -0
- {formative_ds-1.2.2 → formative_ds-2.0.0}/docs/game/single_player.rst +0 -0
- {formative_ds-1.2.2 → formative_ds-2.0.0}/docs/index.rst +0 -0
- {formative_ds-1.2.2 → formative_ds-2.0.0}/examples/decision/01_decision_basic.py +0 -0
- {formative_ds-1.2.2 → formative_ds-2.0.0}/examples/did/01_did_basic.py +0 -0
- {formative_ds-1.2.2 → formative_ds-2.0.0}/examples/iv/01_iv_basic.py +0 -0
- {formative_ds-1.2.2 → formative_ds-2.0.0}/examples/iv/02_iv_vs_ols_bias.py +0 -0
- {formative_ds-1.2.2 → formative_ds-2.0.0}/examples/iv/03_iv_with_observed_confounder.py +0 -0
- {formative_ds-1.2.2 → formative_ds-2.0.0}/examples/iv/04_iv_refutation.py +0 -0
- {formative_ds-1.2.2 → formative_ds-2.0.0}/examples/matching/01_basic_matching.py +0 -0
- {formative_ds-1.2.2 → formative_ds-2.0.0}/examples/matching/02_matching_vs_naive.py +0 -0
- {formative_ds-1.2.2 → formative_ds-2.0.0}/examples/matching/03_matching_refutation.py +0 -0
- {formative_ds-1.2.2 → formative_ds-2.0.0}/examples/ols/01_ols_basic.py +0 -0
- {formative_ds-1.2.2 → formative_ds-2.0.0}/examples/ols/02_ols_unobserved_confounder.py +0 -0
- {formative_ds-1.2.2 → formative_ds-2.0.0}/examples/ols/03_ols_multiple_confounders.py +0 -0
- {formative_ds-1.2.2 → formative_ds-2.0.0}/examples/rct/01_rct_basic.py +0 -0
- {formative_ds-1.2.2 → formative_ds-2.0.0}/examples/rdd/01_rdd_basic.py +0 -0
- {formative_ds-1.2.2 → formative_ds-2.0.0}/examples/rdd/02_rdd_positive_running_var.py +0 -0
- {formative_ds-1.2.2 → formative_ds-2.0.0}/formative/__init__.py +0 -0
- {formative_ds-1.2.2 → formative_ds-2.0.0}/formative/causal/_exceptions.py +0 -0
- {formative_ds-1.2.2 → formative_ds-2.0.0}/formative/causal/dag.py +0 -0
- {formative_ds-1.2.2 → formative_ds-2.0.0}/formative/causal/estimators/__init__.py +0 -0
- {formative_ds-1.2.2 → formative_ds-2.0.0}/formative/causal/estimators/did.py +0 -0
- {formative_ds-1.2.2 → formative_ds-2.0.0}/formative/causal/estimators/iv.py +0 -0
- {formative_ds-1.2.2 → formative_ds-2.0.0}/formative/causal/estimators/matching.py +0 -0
- {formative_ds-1.2.2 → formative_ds-2.0.0}/formative/causal/estimators/rdd.py +0 -0
- {formative_ds-1.2.2 → formative_ds-2.0.0}/formative/causal/refutations/__init__.py +0 -0
- {formative_ds-1.2.2 → formative_ds-2.0.0}/formative/causal/refutations/_check.py +0 -0
- {formative_ds-1.2.2 → formative_ds-2.0.0}/formative/causal/refutations/did.py +0 -0
- {formative_ds-1.2.2 → formative_ds-2.0.0}/formative/causal/refutations/iv.py +0 -0
- {formative_ds-1.2.2 → formative_ds-2.0.0}/formative/causal/refutations/matching.py +0 -0
- {formative_ds-1.2.2 → formative_ds-2.0.0}/formative/causal/refutations/ols.py +0 -0
- {formative_ds-1.2.2 → formative_ds-2.0.0}/formative/causal/refutations/rct.py +0 -0
- {formative_ds-1.2.2 → formative_ds-2.0.0}/formative/causal/refutations/rdd.py +0 -0
- {formative_ds-1.2.2 → formative_ds-2.0.0}/formative/game/expected_value.py +0 -0
- {formative_ds-1.2.2 → formative_ds-2.0.0}/formative/game/hurwicz.py +0 -0
- {formative_ds-1.2.2 → formative_ds-2.0.0}/formative/game/laplace.py +0 -0
- {formative_ds-1.2.2 → formative_ds-2.0.0}/formative/game/maximax.py +0 -0
- {formative_ds-1.2.2 → formative_ds-2.0.0}/formative/game/maximin.py +0 -0
- {formative_ds-1.2.2 → formative_ds-2.0.0}/tests/test_dag.py +0 -0
- {formative_ds-1.2.2 → formative_ds-2.0.0}/tests/test_did.py +0 -0
- {formative_ds-1.2.2 → formative_ds-2.0.0}/tests/test_did_refutation.py +0 -0
- {formative_ds-1.2.2 → formative_ds-2.0.0}/tests/test_iv.py +0 -0
- {formative_ds-1.2.2 → formative_ds-2.0.0}/tests/test_iv_refutation.py +0 -0
- {formative_ds-1.2.2 → formative_ds-2.0.0}/tests/test_matching.py +0 -0
- {formative_ds-1.2.2 → formative_ds-2.0.0}/tests/test_matching_refutation.py +0 -0
- {formative_ds-1.2.2 → formative_ds-2.0.0}/tests/test_ols.py +0 -0
- {formative_ds-1.2.2 → formative_ds-2.0.0}/tests/test_ols_refutation.py +0 -0
- {formative_ds-1.2.2 → formative_ds-2.0.0}/tests/test_rct.py +0 -0
- {formative_ds-1.2.2 → formative_ds-2.0.0}/tests/test_rct_refutation.py +0 -0
- {formative_ds-1.2.2 → formative_ds-2.0.0}/tests/test_rdd.py +0 -0
- {formative_ds-1.2.2 → formative_ds-2.0.0}/tests/test_rdd_refutation.py +0 -0
- {formative_ds-1.2.2 → formative_ds-2.0.0}/vercel.json +0 -0
|
@@ -43,17 +43,24 @@ Called as `result.refute(df)` — always pass the same `df` used for `fit()`. Re
|
|
|
43
43
|
**Refutation seeds** — isolated to avoid collisions with test data (seeded at 42):
|
|
44
44
|
- `_RCC_SEED = 54321` — random common cause column
|
|
45
45
|
- `_PLACEBO_SEED = 99999` — treatment permutation in matching
|
|
46
|
+
- `_PLACEBO_TIME_SEED = 22222` — time permutation in DiD
|
|
47
|
+
- `_PLACEBO_MODIFIER_SEED = 77777` — modifier permutation in CATE checks
|
|
48
|
+
- `_RANDOM_MODIFIER_SEED = 31313` — noise modifier column in CATE checks
|
|
46
49
|
- `_BOOTSTRAP_SEED = 42` — safe to reuse in matching's bootstrap (samples row indices, not independent values)
|
|
47
50
|
|
|
48
51
|
Do not change `_RCC_SEED` or `_PLACEBO_SEED` to 42 — the generated noise collides with instrument/treatment data and produces a singular matrix in IV.
|
|
49
52
|
|
|
53
|
+
**Heterogeneous effects (CATE)** — `OLSObservational` and `RCT` accept `effect_modifier=` (a discrete, pre-treatment column). `fit()` then returns `OLSCATEResult`/`RCTCATEResult` with `effect_by_group`, a homogeneity F-test, and `decide_by_group()`. The headline `effect` is the sample-share-weighted average of group effects (the raw treatment coefficient under interaction coding is only the reference group's effect). Shared machinery lives in `estimators/_cate.py`; DAG validation rejects modifiers that are descendants of the treatment (mediators) or not ancestors of the outcome.
|
|
54
|
+
|
|
50
55
|
**Package layout:**
|
|
51
56
|
- `formative/causal/dag.py` — `DAG`, `_Node`
|
|
52
57
|
- `formative/causal/_exceptions.py` — `IdentificationError`, `GraphError`
|
|
53
|
-
- `formative/causal/estimators/
|
|
58
|
+
- `formative/causal/estimators/_base.py` — `_BaseResult`, `_StatsmodelsResult` (shared result plumbing: effect stats via `_param`, `decide()`, assumptions footer)
|
|
59
|
+
- `formative/causal/estimators/_cate.py` — `GroupEffect`, `_fit_cate`, `_CATEResultMixin`, modifier validation (shared by OLS/RCT and the CATE refutations)
|
|
60
|
+
- `formative/causal/estimators/ols.py` — `OLSObservational`, `OLSResult`, `OLSCATEResult`
|
|
54
61
|
- `formative/causal/estimators/iv.py` — `IV2SLS`, `IVResult`
|
|
55
62
|
- `formative/causal/estimators/matching.py` — `PropensityScoreMatching`, `MatchingResult`; exports `_propensity_scores`, `_att_from_ps` for use by refutations
|
|
56
|
-
- `formative/causal/estimators/rct.py` — `RCT`, `RCTResult`
|
|
63
|
+
- `formative/causal/estimators/rct.py` — `RCT`, `RCTResult`, `RCTCATEResult`
|
|
57
64
|
- `formative/causal/estimators/did.py` — `DiD`, `DiDResult`
|
|
58
65
|
- `formative/causal/estimators/rdd.py` — `RDD`, `RDDResult`
|
|
59
66
|
- `formative/causal/refutations/_check.py` — `Assumption`, `RefutationCheck`, `RefutationReport`
|
|
@@ -61,6 +68,7 @@ Do not change `_RCC_SEED` or `_PLACEBO_SEED` to 42 — the generated noise colli
|
|
|
61
68
|
- `formative/causal/refutations/iv.py` — `IVRefutationReport`, `_check_first_stage_f`, `_check_random_common_cause`
|
|
62
69
|
- `formative/causal/refutations/matching.py` — `MatchingRefutationReport`, `_check_placebo_treatment`, `_check_random_common_cause`
|
|
63
70
|
- `formative/causal/refutations/rct.py` — `RCTRefutationReport`, `_check_random_common_cause`
|
|
71
|
+
- `formative/causal/refutations/cate.py` — `_check_placebo_modifier`, `_check_random_modifier`, `_check_random_common_cause` (used by `OLSCATEResult`/`RCTCATEResult`; reports reuse the OLS/RCT report classes)
|
|
64
72
|
- `formative/causal/refutations/did.py` — `DiDRefutationReport`, `_check_placebo_group`, `_check_random_common_cause`
|
|
65
73
|
- `formative/causal/refutations/rdd.py` — `RDDRefutationReport`, `_check_placebo_cutoff`, `_check_random_common_cause`
|
|
66
74
|
- `formative/causal/_explain.py` — narrative rendering for all estimators
|
|
@@ -91,7 +99,7 @@ result = maximin({
|
|
|
91
99
|
**Package layout:**
|
|
92
100
|
- `formative/game/maximin.py` — `maximin`, `Maximin`, `MaximinResult`
|
|
93
101
|
- `formative/game/maximax.py` — `maximax`, `Maximax`, `MaximaxResult`
|
|
94
|
-
- `formative/game/
|
|
102
|
+
- `formative/game/minimax.py` — `minimax`, `Minimax`, `MinimaxResult`
|
|
95
103
|
- `formative/game/hurwicz.py` — `hurwicz`, `Hurwicz`, `HurwiczResult`
|
|
96
104
|
- `formative/game/laplace.py` — `laplace`, `Laplace`, `LaplaceResult`
|
|
97
105
|
- `formative/game/expected_value.py` — `expected_value`, `ExpectedValue`, `ExpectedValueResult`
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: formative-ds
|
|
3
|
-
Version:
|
|
3
|
+
Version: 2.0.0
|
|
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
|
|
@@ -68,7 +68,7 @@ Confounders declared in the DAG are controlled for automatically. If a confounde
|
|
|
68
68
|
### Decision rules
|
|
69
69
|
|
|
70
70
|
```python
|
|
71
|
-
from formative.game import maximin, maximax, hurwicz, laplace,
|
|
71
|
+
from formative.game import maximin, maximax, hurwicz, laplace, minimax
|
|
72
72
|
|
|
73
73
|
outcomes = {
|
|
74
74
|
"stocks": {"recession": -20, "stagnation": 5, "growth": 30},
|
|
@@ -80,7 +80,7 @@ maximin(outcomes).solve() # safest choice (best worst case)
|
|
|
80
80
|
maximax(outcomes).solve() # most optimistic (best best case)
|
|
81
81
|
hurwicz(outcomes, alpha=0.5).solve() # blend of optimism and pessimism
|
|
82
82
|
laplace(outcomes).solve() # highest average payoff
|
|
83
|
-
|
|
83
|
+
minimax(outcomes).solve() # lowest worst-case regret
|
|
84
84
|
```
|
|
85
85
|
|
|
86
86
|
See online documentation at [docs.getformative.dev](https://docs.getformative.dev) for more examples and details.
|
|
@@ -41,7 +41,7 @@ Confounders declared in the DAG are controlled for automatically. If a confounde
|
|
|
41
41
|
### Decision rules
|
|
42
42
|
|
|
43
43
|
```python
|
|
44
|
-
from formative.game import maximin, maximax, hurwicz, laplace,
|
|
44
|
+
from formative.game import maximin, maximax, hurwicz, laplace, minimax
|
|
45
45
|
|
|
46
46
|
outcomes = {
|
|
47
47
|
"stocks": {"recession": -20, "stagnation": 5, "growth": 30},
|
|
@@ -53,7 +53,7 @@ maximin(outcomes).solve() # safest choice (best worst case)
|
|
|
53
53
|
maximax(outcomes).solve() # most optimistic (best best case)
|
|
54
54
|
hurwicz(outcomes, alpha=0.5).solve() # blend of optimism and pessimism
|
|
55
55
|
laplace(outcomes).solve() # highest average payoff
|
|
56
|
-
|
|
56
|
+
minimax(outcomes).solve() # lowest worst-case regret
|
|
57
57
|
```
|
|
58
58
|
|
|
59
59
|
See online documentation at [docs.getformative.dev](https://docs.getformative.dev) for more examples and details.
|
|
@@ -36,7 +36,7 @@ earnings using OLS with a DAG that encodes family background as a confounder:
|
|
|
36
36
|
|
|
37
37
|
import numpy as np
|
|
38
38
|
import pandas as pd
|
|
39
|
-
from formative import DAG, OLSObservational
|
|
39
|
+
from formative.causal import DAG, OLSObservational
|
|
40
40
|
|
|
41
41
|
rng = np.random.default_rng(0)
|
|
42
42
|
N = 2_000
|
|
@@ -95,7 +95,7 @@ to any rule in ``formative.game``:
|
|
|
95
95
|
|
|
96
96
|
.. code-block:: python
|
|
97
97
|
|
|
98
|
-
from formative.game import maximin,
|
|
98
|
+
from formative.game import maximin, minimax, hurwicz
|
|
99
99
|
|
|
100
100
|
decision = result.decide(cost=8, benefit=15)
|
|
101
101
|
outcomes = decision.to_outcomes()
|
|
@@ -105,7 +105,7 @@ to any rule in ``formative.game``:
|
|
|
105
105
|
# }
|
|
106
106
|
|
|
107
107
|
maximin(outcomes).solve() # best worst-case
|
|
108
|
-
|
|
108
|
+
minimax(outcomes).solve() # minimise maximum regret
|
|
109
109
|
hurwicz(outcomes, alpha=0.3).solve() # weighted pessimism–optimism
|
|
110
110
|
|
|
111
111
|
By default, the three scenarios correspond to the 10th, 50th, and 90th
|
|
@@ -127,27 +127,6 @@ set to the CI bounds (quantiles 0.025 and 0.975). ``to_outcomes()``
|
|
|
127
127
|
generalises that check: different rules express different risk attitudes, and
|
|
128
128
|
you can dial in your own pessimism level via ``hurwicz(alpha=...)``.
|
|
129
129
|
|
|
130
|
-
Value of information
|
|
131
|
-
--------------------
|
|
132
|
-
|
|
133
|
-
When a decision is *not* already highly confident, it is useful to know how much
|
|
134
|
-
more data would be needed to reach a target confidence level. Call
|
|
135
|
-
``value_of_information()`` on the report:
|
|
136
|
-
|
|
137
|
-
.. code-block:: python
|
|
138
|
-
|
|
139
|
-
print(decision.value_of_information(target_confidence=0.99))
|
|
140
|
-
|
|
141
|
-
If the decision is already at or above the target confidence, formative reports
|
|
142
|
-
that no additional data is needed. Otherwise, it reports:
|
|
143
|
-
|
|
144
|
-
* how much the standard error on net benefit would need to shrink, and
|
|
145
|
-
* approximately how many times larger the sample would need to be.
|
|
146
|
-
|
|
147
|
-
This frames the cost of uncertainty concretely: collecting more data has a price,
|
|
148
|
-
and the value of information tells you whether that price is worth paying before
|
|
149
|
-
you commit to a decision.
|
|
150
|
-
|
|
151
130
|
Philosophy
|
|
152
131
|
----------
|
|
153
132
|
|
|
@@ -2,7 +2,7 @@ Estimands: ATE, ATT, LATE etc.
|
|
|
2
2
|
==============================
|
|
3
3
|
|
|
4
4
|
Every causal estimator targets a specific *estimand*. Choosing the wrong estimator for your question gives a valid answer to the wrong
|
|
5
|
-
question. The
|
|
5
|
+
question. The five estimands formative works with are ATE, ATT, LATE, LATE at the cutoff, and CATE. One method can only target one estimand.
|
|
6
6
|
|
|
7
7
|
.. list-table::
|
|
8
8
|
:header-rows: 1
|
|
@@ -14,6 +14,9 @@ question. The four estimands formative works with are ATE, ATT, LATE, and LATE a
|
|
|
14
14
|
* - **ATE**
|
|
15
15
|
- Average Treatment Effect
|
|
16
16
|
- :class:`~formative.OLSObservational`, :class:`~formative.RCT`
|
|
17
|
+
* - **CATE**
|
|
18
|
+
- Conditional Average Treatment Effect
|
|
19
|
+
- :class:`~formative.OLSObservational`, :class:`~formative.RCT` (with ``effect_modifier``)
|
|
17
20
|
* - **ATT**
|
|
18
21
|
- Average Treatment Effect on the Treated
|
|
19
22
|
- :class:`~formative.PropensityScoreMatching`, :class:`~formative.DiD`
|
|
@@ -191,3 +194,48 @@ assumption is more defensible but the estimate has higher variance.
|
|
|
191
194
|
- **RDD** — fits a local linear regression on both sides of the cutoff. The coefficient on
|
|
192
195
|
the treatment indicator gives the jump in outcome at the threshold, controlling for the
|
|
193
196
|
slope of the running variable separately on each side.
|
|
197
|
+
|
|
198
|
+
----
|
|
199
|
+
|
|
200
|
+
CATE: Conditional Average Treatment Effect
|
|
201
|
+
------------------------------------------
|
|
202
|
+
|
|
203
|
+
**What it answers:** *For whom* does the treatment work? An average effect answers "does the
|
|
204
|
+
intervention work?"; the CATE answers how the effect differs across known subgroups — should
|
|
205
|
+
a discount go to new customers or loyal ones, does the training help juniors more than
|
|
206
|
+
seniors?
|
|
207
|
+
|
|
208
|
+
.. math::
|
|
209
|
+
|
|
210
|
+
\text{CATE}(x) = \mathbb{E}[Y(1) - Y(0) \mid X = x]
|
|
211
|
+
|
|
212
|
+
:math:`X` is an *effect modifier*: a pre-treatment variable whose levels the effect is
|
|
213
|
+
allowed to differ across. Instead of one number, the estimand is a per-group effect —
|
|
214
|
+
one for each level of :math:`X`.
|
|
215
|
+
|
|
216
|
+
**When it makes sense:** When the decision is not "treat everyone or no one" but "who should
|
|
217
|
+
receive the treatment" — targeting, personalisation, prioritising a limited budget. Estimating
|
|
218
|
+
per-group effects also comes with a **homogeneity test** answering whether the group
|
|
219
|
+
differences are real or noise; without it, eyeballing subgroup estimates invites false
|
|
220
|
+
discoveries.
|
|
221
|
+
|
|
222
|
+
In formative, pass ``effect_modifier=`` to :class:`~formative.OLSObservational` or
|
|
223
|
+
:class:`~formative.RCT`. The fitted model becomes
|
|
224
|
+
``outcome ~ treatment * C(modifier) + controls``, and the result exposes per-group effects
|
|
225
|
+
(``effect_by_group``), the homogeneity test, and a cost-benefit decision per group
|
|
226
|
+
(``decide_by_group``). The modifier must be measured pre-treatment: segmenting by a variable
|
|
227
|
+
the treatment itself affects (a mediator) manufactures artificial heterogeneity, and the DAG
|
|
228
|
+
validation rejects modifiers that are descendants of the treatment.
|
|
229
|
+
|
|
230
|
+
**Methods that estimate CATE:**
|
|
231
|
+
|
|
232
|
+
- **OLS Observational** (with ``effect_modifier``) — interacts the treatment with the
|
|
233
|
+
modifier while adjusting for confounders identified via the backdoor criterion.
|
|
234
|
+
- **RCT** (with ``effect_modifier``) — randomisation makes each subgroup's simple contrast
|
|
235
|
+
unbiased; the interaction model estimates all subgroups jointly.
|
|
236
|
+
|
|
237
|
+
**CATE vs ATE:** The ATE is the average of the CATEs, weighted by each group's share of the
|
|
238
|
+
population — formative reports exactly this weighted average as the headline effect when an
|
|
239
|
+
effect modifier is used. A homogeneous effect means CATE(x) = ATE for every group; the wider
|
|
240
|
+
the CATEs spread around the ATE, the more value there is in targeting rather than treating
|
|
241
|
+
uniformly.
|
|
@@ -14,6 +14,13 @@ OLSResult
|
|
|
14
14
|
:members:
|
|
15
15
|
:inherited-members:
|
|
16
16
|
|
|
17
|
+
OLSCATEResult
|
|
18
|
+
-------------
|
|
19
|
+
|
|
20
|
+
.. autoclass:: formative.causal.OLSCATEResult
|
|
21
|
+
:members:
|
|
22
|
+
:inherited-members:
|
|
23
|
+
|
|
17
24
|
IV2SLS
|
|
18
25
|
------
|
|
19
26
|
|
|
@@ -53,6 +60,13 @@ RCTResult
|
|
|
53
60
|
:members:
|
|
54
61
|
:inherited-members:
|
|
55
62
|
|
|
63
|
+
RCTCATEResult
|
|
64
|
+
-------------
|
|
65
|
+
|
|
66
|
+
.. autoclass:: formative.causal.RCTCATEResult
|
|
67
|
+
:members:
|
|
68
|
+
:inherited-members:
|
|
69
|
+
|
|
56
70
|
DiD
|
|
57
71
|
---
|
|
58
72
|
|
|
@@ -78,3 +92,9 @@ RDDResult
|
|
|
78
92
|
.. autoclass:: formative.causal.RDDResult
|
|
79
93
|
:members:
|
|
80
94
|
:inherited-members:
|
|
95
|
+
|
|
96
|
+
GroupEffect
|
|
97
|
+
-----------
|
|
98
|
+
|
|
99
|
+
.. autoclass:: formative.causal.GroupEffect
|
|
100
|
+
:members:
|
|
@@ -74,8 +74,8 @@ downside.
|
|
|
74
74
|
.. autoclass:: formative.game.MaximaxResult
|
|
75
75
|
:members:
|
|
76
76
|
|
|
77
|
-
Minimax
|
|
78
|
-
|
|
77
|
+
Minimax
|
|
78
|
+
-------
|
|
79
79
|
|
|
80
80
|
*Which choice do you regret the least in hindsight?*
|
|
81
81
|
|
|
@@ -115,14 +115,14 @@ Then compute regret for every combination — how much you miss out on versus th
|
|
|
115
115
|
|
|
116
116
|
.. code-block:: python
|
|
117
117
|
|
|
118
|
-
from formative.game import
|
|
118
|
+
from formative.game import minimax
|
|
119
119
|
|
|
120
|
-
result =
|
|
120
|
+
result = minimax(outcomes).solve()
|
|
121
121
|
print(result)
|
|
122
122
|
|
|
123
123
|
.. code-block:: text
|
|
124
124
|
|
|
125
|
-
|
|
125
|
+
MinimaxResult(
|
|
126
126
|
stocks max regret: +25
|
|
127
127
|
bonds max regret: +23 ← chosen
|
|
128
128
|
cash max regret: +28
|
|
@@ -133,10 +133,10 @@ score zero in recession and stagnation, but 23 in growth where stocks outperform
|
|
|
133
133
|
Cash never comes close to the best option in any scenario, giving it the highest max
|
|
134
134
|
regret of 28. Bonds minimise the worst-case regret.
|
|
135
135
|
|
|
136
|
-
.. autoclass:: formative.game.
|
|
136
|
+
.. autoclass:: formative.game.Minimax
|
|
137
137
|
:members:
|
|
138
138
|
|
|
139
|
-
.. autoclass:: formative.game.
|
|
139
|
+
.. autoclass:: formative.game.MinimaxResult
|
|
140
140
|
:members:
|
|
141
141
|
|
|
142
142
|
Hurwicz Criterion
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
from .dag import DAG
|
|
2
2
|
from .decision import DecisionReport
|
|
3
|
+
from .estimators._cate import GroupEffect
|
|
3
4
|
from .estimators.did import DiD, DiDResult
|
|
4
5
|
from .estimators.iv import IV2SLS, IVResult
|
|
5
6
|
from .estimators.matching import MatchingResult, PropensityScoreMatching
|
|
6
|
-
from .estimators.ols import OLSObservational, OLSResult
|
|
7
|
-
from .estimators.rct import RCT, RCTResult
|
|
7
|
+
from .estimators.ols import OLSCATEResult, OLSObservational, OLSResult
|
|
8
|
+
from .estimators.rct import RCT, RCTCATEResult, RCTResult
|
|
8
9
|
from .estimators.rdd import RDD, RDDResult
|
|
9
10
|
from .refutations import (
|
|
10
11
|
DiDRefutationReport,
|
|
@@ -22,12 +23,15 @@ __all__ = [
|
|
|
22
23
|
"DAG",
|
|
23
24
|
"OLSObservational",
|
|
24
25
|
"OLSResult",
|
|
26
|
+
"OLSCATEResult",
|
|
27
|
+
"GroupEffect",
|
|
25
28
|
"IV2SLS",
|
|
26
29
|
"IVResult",
|
|
27
30
|
"PropensityScoreMatching",
|
|
28
31
|
"MatchingResult",
|
|
29
32
|
"RCT",
|
|
30
33
|
"RCTResult",
|
|
34
|
+
"RCTCATEResult",
|
|
31
35
|
"DiD",
|
|
32
36
|
"DiDResult",
|
|
33
37
|
"RDD",
|
|
@@ -82,6 +82,46 @@ def _dag_section(dag, treatment: str, outcome: str, adjustment_set: set[str]) ->
|
|
|
82
82
|
return "\n".join(lines)
|
|
83
83
|
|
|
84
84
|
|
|
85
|
+
def _heterogeneity_section(result) -> str | None:
|
|
86
|
+
"""HETEROGENEITY block for results fitted with an effect modifier, else None."""
|
|
87
|
+
modifier = getattr(result, "_modifier", None)
|
|
88
|
+
if modifier is None:
|
|
89
|
+
return None
|
|
90
|
+
|
|
91
|
+
groups = result.effect_by_group
|
|
92
|
+
p = result.homogeneity_pvalue
|
|
93
|
+
largest = max(groups, key=lambda g: g.effect)
|
|
94
|
+
smallest = min(groups, key=lambda g: g.effect)
|
|
95
|
+
|
|
96
|
+
if p < 0.05:
|
|
97
|
+
verdict = (
|
|
98
|
+
f"A formal test rejects the hypothesis that the effect is the same across "
|
|
99
|
+
f"{modifier} groups ({_fmt_p(p)}): the treatment effect genuinely differs by {modifier}."
|
|
100
|
+
)
|
|
101
|
+
else:
|
|
102
|
+
verdict = (
|
|
103
|
+
f"A formal test does not reject the hypothesis that the effect is the same "
|
|
104
|
+
f"across {modifier} groups ({_fmt_p(p)}); the differences below may be noise."
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
lines = [
|
|
108
|
+
"HETEROGENEITY",
|
|
109
|
+
f"The effect was allowed to differ across levels of {modifier}. {verdict}",
|
|
110
|
+
"",
|
|
111
|
+
]
|
|
112
|
+
for g in groups:
|
|
113
|
+
lo, hi = g.conf_int
|
|
114
|
+
lines.append(f" • {modifier} = {g.level}: effect = {g.effect:.4f} (95% CI: {_fmt_ci(lo, hi)}, n = {g.n:,})")
|
|
115
|
+
lines += [
|
|
116
|
+
"",
|
|
117
|
+
f"The largest estimated effect is for {modifier} = {largest.level} ({largest.effect:.4f}) "
|
|
118
|
+
f"and the smallest for {modifier} = {smallest.level} ({smallest.effect:.4f}). Group "
|
|
119
|
+
f"estimates are less precise than the overall average, and with many groups some "
|
|
120
|
+
f"extreme-looking effects are expected by chance.",
|
|
121
|
+
]
|
|
122
|
+
return "\n".join(lines)
|
|
123
|
+
|
|
124
|
+
|
|
85
125
|
def _assumptions_section(assumptions: list) -> str:
|
|
86
126
|
n = len(assumptions)
|
|
87
127
|
n_u = sum(1 for a in assumptions if not a.testable)
|
|
@@ -115,6 +155,7 @@ def explain_ols(result) -> str:
|
|
|
115
155
|
adj = sorted(result._adjustment_set)
|
|
116
156
|
lo, hi = result.conf_int
|
|
117
157
|
bias = result.unadjusted_effect - result.effect
|
|
158
|
+
heterogeneity = _heterogeneity_section(result)
|
|
118
159
|
|
|
119
160
|
bias_lines = []
|
|
120
161
|
if adj:
|
|
@@ -161,6 +202,7 @@ def explain_ols(result) -> str:
|
|
|
161
202
|
*bias_lines,
|
|
162
203
|
]
|
|
163
204
|
),
|
|
205
|
+
*([heterogeneity] if heterogeneity else []),
|
|
164
206
|
"\n".join(
|
|
165
207
|
[
|
|
166
208
|
"CAVEATS",
|
|
@@ -247,6 +289,7 @@ def explain_iv(result) -> str:
|
|
|
247
289
|
def explain_rct(result) -> str:
|
|
248
290
|
T, Y = result._treatment, result._outcome
|
|
249
291
|
lo, hi = result.conf_int
|
|
292
|
+
heterogeneity = _heterogeneity_section(result)
|
|
250
293
|
|
|
251
294
|
blocks = [
|
|
252
295
|
"\n".join(
|
|
@@ -277,6 +320,7 @@ def explain_rct(result) -> str:
|
|
|
277
320
|
f"SE = {result.std_err:.4f}, {_fmt_p(result.pvalue)}).",
|
|
278
321
|
]
|
|
279
322
|
),
|
|
323
|
+
*([heterogeneity] if heterogeneity else []),
|
|
280
324
|
"\n".join(
|
|
281
325
|
[
|
|
282
326
|
"CAVEATS",
|
|
@@ -46,51 +46,6 @@ class DecisionReport:
|
|
|
46
46
|
robust: bool
|
|
47
47
|
p_beneficial: float
|
|
48
48
|
|
|
49
|
-
def value_of_information(self, target_confidence: float = 0.95) -> str:
|
|
50
|
-
"""
|
|
51
|
-
Estimate how much the standard error would need to shrink — and
|
|
52
|
-
approximately how many times larger the sample would need to be — to
|
|
53
|
-
reach ``target_confidence`` that the optimal decision is correct.
|
|
54
|
-
|
|
55
|
-
If the decision is already at or above ``target_confidence``, reports
|
|
56
|
-
that no additional data is needed.
|
|
57
|
-
|
|
58
|
-
Parameters
|
|
59
|
-
----------
|
|
60
|
-
target_confidence : float
|
|
61
|
-
Desired probability that net benefit > 0 (default 0.95).
|
|
62
|
-
|
|
63
|
-
Returns
|
|
64
|
-
-------
|
|
65
|
-
str
|
|
66
|
-
A human-readable description of the value of information.
|
|
67
|
-
"""
|
|
68
|
-
if self.p_beneficial >= target_confidence:
|
|
69
|
-
return (
|
|
70
|
-
f"Decision confidence is already {self.p_beneficial:.1%}, "
|
|
71
|
-
f"at or above the target of {target_confidence:.1%}. "
|
|
72
|
-
f"No additional data is needed to be confident in this decision."
|
|
73
|
-
)
|
|
74
|
-
|
|
75
|
-
# net_benefit / (se_net * z_target) = 1 → se_net_required = net_benefit / z_target
|
|
76
|
-
# se_net = std_err_ate * benefit → std_err_required = se_net_required / benefit
|
|
77
|
-
z_target = norm.ppf(target_confidence)
|
|
78
|
-
se_net_current = abs(self.net_benefit_ci[1] - self.net_benefit_ci[0]) / (2 * 1.96)
|
|
79
|
-
if se_net_current == 0:
|
|
80
|
-
return "Cannot compute value of information: standard error is zero."
|
|
81
|
-
|
|
82
|
-
se_net_required = abs(self.net_benefit) / z_target
|
|
83
|
-
se_ratio = se_net_current / se_net_required
|
|
84
|
-
n_multiplier = se_ratio**2
|
|
85
|
-
|
|
86
|
-
return (
|
|
87
|
-
f"Current decision confidence: {self.p_beneficial:.1%} "
|
|
88
|
-
f"(target: {target_confidence:.1%}).\n"
|
|
89
|
-
f"The standard error on net benefit would need to shrink by "
|
|
90
|
-
f"{(1 - 1 / se_ratio):.1%} to reach the target.\n"
|
|
91
|
-
f"This requires approximately {n_multiplier:.1f}x the current sample size."
|
|
92
|
-
)
|
|
93
|
-
|
|
94
49
|
def to_outcomes(
|
|
95
50
|
self,
|
|
96
51
|
scenarios: dict[str, float] | None = None,
|
|
@@ -110,7 +65,7 @@ class DecisionReport:
|
|
|
110
65
|
-------
|
|
111
66
|
dict[str, dict[str, float]]
|
|
112
67
|
A ``{choice: {scenario: payoff}}`` structure accepted by any
|
|
113
|
-
``formative.game`` rule (``maximin``, ``
|
|
68
|
+
``formative.game`` rule (``maximin``, ``minimax``,
|
|
114
69
|
``hurwicz``, etc.). The ``"don't treat"`` payoff is 0 in every
|
|
115
70
|
scenario — the status quo baseline.
|
|
116
71
|
|
|
@@ -29,6 +29,10 @@ class _BaseResult:
|
|
|
29
29
|
lines.append("")
|
|
30
30
|
return lines
|
|
31
31
|
|
|
32
|
+
def _extra_summary_lines(self) -> list[str]:
|
|
33
|
+
"""Hook for subclasses to insert extra blocks before the assumptions footer."""
|
|
34
|
+
return []
|
|
35
|
+
|
|
32
36
|
def decide(self, cost: float, benefit: float):
|
|
33
37
|
"""
|
|
34
38
|
Compute a cost-benefit decision analysis from this causal estimate.
|