formative-ds 2.1.0__tar.gz → 2.2.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-2.2.0/.gitattributes +1 -0
- {formative_ds-2.1.0 → formative_ds-2.2.0}/CLAUDE.md +21 -1
- formative_ds-2.2.0/Makefile +29 -0
- {formative_ds-2.1.0 → formative_ds-2.2.0}/PKG-INFO +21 -12
- {formative_ds-2.1.0 → formative_ds-2.2.0}/README.md +20 -11
- formative_ds-2.2.0/docs/_static/version_switcher.css +11 -0
- formative_ds-2.2.0/docs/_static/version_switcher.js +46 -0
- formative_ds-2.2.0/docs/causal/decisions.rst +458 -0
- formative_ds-2.2.0/docs/causal/estimands.rst +241 -0
- formative_ds-2.2.0/docs/causal/introduction.rst +178 -0
- {formative_ds-2.1.0 → formative_ds-2.2.0}/docs/conf.py +14 -0
- formative_ds-2.2.0/docs/design/policy_learning.md +128 -0
- {formative_ds-2.1.0 → formative_ds-2.2.0}/formative/causal/__init__.py +5 -0
- {formative_ds-2.1.0 → formative_ds-2.2.0}/formative/causal/_explain.py +76 -0
- formative_ds-2.2.0/formative/causal/estimators/policy.py +491 -0
- {formative_ds-2.1.0 → formative_ds-2.2.0}/formative/causal/estimators/rct.py +45 -0
- {formative_ds-2.1.0 → formative_ds-2.2.0}/formative/causal/refutations/__init__.py +2 -0
- formative_ds-2.2.0/formative/causal/refutations/policy.py +117 -0
- {formative_ds-2.1.0 → formative_ds-2.2.0}/pyproject.toml +9 -2
- formative_ds-2.2.0/scripts/snapshot_docs.py +72 -0
- formative_ds-2.2.0/site/2.1/_sources/causal/benchmarks.rst.txt +414 -0
- formative_ds-2.2.0/site/2.1/_sources/causal/dag.rst.txt +9 -0
- formative_ds-2.2.0/site/2.1/_sources/causal/estimators.rst.txt +100 -0
- formative_ds-2.2.0/site/2.1/_sources/causal/motivation.rst.txt +57 -0
- formative_ds-2.2.0/site/2.1/_sources/causal/refutations.rst.txt +54 -0
- formative_ds-2.2.0/site/2.1/_sources/causal/wizard.rst.txt +9 -0
- formative_ds-2.2.0/site/2.1/_sources/game/decision_rules.rst.txt +271 -0
- formative_ds-2.2.0/site/2.1/_sources/game/introduction.rst.txt +39 -0
- formative_ds-2.2.0/site/2.1/_sources/game/single_player.rst.txt +10 -0
- formative_ds-2.2.0/site/2.1/_sources/index.rst.txt +40 -0
- formative_ds-2.2.0/site/2.1/_static/_sphinx_javascript_frameworks_compat.js +123 -0
- formative_ds-2.2.0/site/2.1/_static/base-stemmer.js +476 -0
- formative_ds-2.2.0/site/2.1/_static/basic.css +906 -0
- formative_ds-2.2.0/site/2.1/_static/check-solid.svg +4 -0
- formative_ds-2.2.0/site/2.1/_static/clipboard.min.js +7 -0
- formative_ds-2.2.0/site/2.1/_static/copy-button.svg +5 -0
- formative_ds-2.2.0/site/2.1/_static/copybutton.css +94 -0
- formative_ds-2.2.0/site/2.1/_static/copybutton.js +248 -0
- formative_ds-2.2.0/site/2.1/_static/copybutton_funcs.js +73 -0
- formative_ds-2.2.0/site/2.1/_static/css/badge_only.css +1 -0
- formative_ds-2.2.0/site/2.1/_static/css/fonts/Roboto-Slab-Bold.woff +0 -0
- formative_ds-2.2.0/site/2.1/_static/css/fonts/Roboto-Slab-Bold.woff2 +0 -0
- formative_ds-2.2.0/site/2.1/_static/css/fonts/Roboto-Slab-Regular.woff +0 -0
- formative_ds-2.2.0/site/2.1/_static/css/fonts/Roboto-Slab-Regular.woff2 +0 -0
- formative_ds-2.2.0/site/2.1/_static/css/fonts/fontawesome-webfont.eot +0 -0
- formative_ds-2.2.0/site/2.1/_static/css/fonts/fontawesome-webfont.svg +2671 -0
- formative_ds-2.2.0/site/2.1/_static/css/fonts/fontawesome-webfont.ttf +0 -0
- formative_ds-2.2.0/site/2.1/_static/css/fonts/fontawesome-webfont.woff +0 -0
- formative_ds-2.2.0/site/2.1/_static/css/fonts/fontawesome-webfont.woff2 +0 -0
- formative_ds-2.2.0/site/2.1/_static/css/fonts/lato-bold-italic.woff +0 -0
- formative_ds-2.2.0/site/2.1/_static/css/fonts/lato-bold-italic.woff2 +0 -0
- formative_ds-2.2.0/site/2.1/_static/css/fonts/lato-bold.woff +0 -0
- formative_ds-2.2.0/site/2.1/_static/css/fonts/lato-bold.woff2 +0 -0
- formative_ds-2.2.0/site/2.1/_static/css/fonts/lato-normal-italic.woff +0 -0
- formative_ds-2.2.0/site/2.1/_static/css/fonts/lato-normal-italic.woff2 +0 -0
- formative_ds-2.2.0/site/2.1/_static/css/fonts/lato-normal.woff +0 -0
- formative_ds-2.2.0/site/2.1/_static/css/fonts/lato-normal.woff2 +0 -0
- formative_ds-2.2.0/site/2.1/_static/css/theme.css +4 -0
- formative_ds-2.2.0/site/2.1/_static/doctools.js +150 -0
- formative_ds-2.2.0/site/2.1/_static/documentation_options.js +13 -0
- formative_ds-2.2.0/site/2.1/_static/english-stemmer.js +1066 -0
- formative_ds-2.2.0/site/2.1/_static/file.png +0 -0
- formative_ds-2.2.0/site/2.1/_static/fonts/Lato/lato-bold.eot +0 -0
- formative_ds-2.2.0/site/2.1/_static/fonts/Lato/lato-bold.ttf +0 -0
- formative_ds-2.2.0/site/2.1/_static/fonts/Lato/lato-bold.woff +0 -0
- formative_ds-2.2.0/site/2.1/_static/fonts/Lato/lato-bold.woff2 +0 -0
- formative_ds-2.2.0/site/2.1/_static/fonts/Lato/lato-bolditalic.eot +0 -0
- formative_ds-2.2.0/site/2.1/_static/fonts/Lato/lato-bolditalic.ttf +0 -0
- formative_ds-2.2.0/site/2.1/_static/fonts/Lato/lato-bolditalic.woff +0 -0
- formative_ds-2.2.0/site/2.1/_static/fonts/Lato/lato-bolditalic.woff2 +0 -0
- formative_ds-2.2.0/site/2.1/_static/fonts/Lato/lato-italic.eot +0 -0
- formative_ds-2.2.0/site/2.1/_static/fonts/Lato/lato-italic.ttf +0 -0
- formative_ds-2.2.0/site/2.1/_static/fonts/Lato/lato-italic.woff +0 -0
- formative_ds-2.2.0/site/2.1/_static/fonts/Lato/lato-italic.woff2 +0 -0
- formative_ds-2.2.0/site/2.1/_static/fonts/Lato/lato-regular.eot +0 -0
- formative_ds-2.2.0/site/2.1/_static/fonts/Lato/lato-regular.ttf +0 -0
- formative_ds-2.2.0/site/2.1/_static/fonts/Lato/lato-regular.woff +0 -0
- formative_ds-2.2.0/site/2.1/_static/fonts/Lato/lato-regular.woff2 +0 -0
- formative_ds-2.2.0/site/2.1/_static/fonts/RobotoSlab/roboto-slab-v7-bold.eot +0 -0
- formative_ds-2.2.0/site/2.1/_static/fonts/RobotoSlab/roboto-slab-v7-bold.ttf +0 -0
- formative_ds-2.2.0/site/2.1/_static/fonts/RobotoSlab/roboto-slab-v7-bold.woff +0 -0
- formative_ds-2.2.0/site/2.1/_static/fonts/RobotoSlab/roboto-slab-v7-bold.woff2 +0 -0
- formative_ds-2.2.0/site/2.1/_static/fonts/RobotoSlab/roboto-slab-v7-regular.eot +0 -0
- formative_ds-2.2.0/site/2.1/_static/fonts/RobotoSlab/roboto-slab-v7-regular.ttf +0 -0
- formative_ds-2.2.0/site/2.1/_static/fonts/RobotoSlab/roboto-slab-v7-regular.woff +0 -0
- formative_ds-2.2.0/site/2.1/_static/fonts/RobotoSlab/roboto-slab-v7-regular.woff2 +0 -0
- formative_ds-2.2.0/site/2.1/_static/jquery.js +2 -0
- formative_ds-2.2.0/site/2.1/_static/js/badge_only.js +1 -0
- formative_ds-2.2.0/site/2.1/_static/js/theme.js +1 -0
- formative_ds-2.2.0/site/2.1/_static/js/versions.js +228 -0
- formative_ds-2.2.0/site/2.1/_static/language_data.js +13 -0
- formative_ds-2.2.0/site/2.1/_static/minus.png +0 -0
- formative_ds-2.2.0/site/2.1/_static/plus.png +0 -0
- formative_ds-2.2.0/site/2.1/_static/pygments.css +75 -0
- formative_ds-2.2.0/site/2.1/_static/searchtools.js +693 -0
- formative_ds-2.2.0/site/2.1/_static/sphinx_highlight.js +159 -0
- formative_ds-2.2.0/site/2.1/_static/version_switcher.css +11 -0
- formative_ds-2.2.0/site/2.1/_static/version_switcher.js +46 -0
- formative_ds-2.2.0/site/2.1/causal/benchmarks.html +547 -0
- formative_ds-2.2.0/site/2.1/causal/dag.html +296 -0
- formative_ds-2.2.0/site/2.1/causal/decisions.html +440 -0
- formative_ds-2.2.0/site/2.1/causal/estimands.html +344 -0
- formative_ds-2.2.0/site/2.1/causal/estimators.html +1957 -0
- formative_ds-2.2.0/site/2.1/causal/introduction.html +212 -0
- formative_ds-2.2.0/site/2.1/causal/motivation.html +175 -0
- formative_ds-2.2.0/site/2.1/causal/refutations.html +396 -0
- formative_ds-2.2.0/site/2.1/causal/wizard.html +133 -0
- formative_ds-2.2.0/site/2.1/game/decision_rules.html +892 -0
- formative_ds-2.2.0/site/2.1/game/introduction.html +156 -0
- formative_ds-2.2.0/site/2.1/game/single_player.html +135 -0
- formative_ds-2.2.0/site/2.1/genindex.html +754 -0
- formative_ds-2.2.0/site/2.1/index.html +140 -0
- formative_ds-2.2.0/site/2.1/objects.inv +0 -0
- formative_ds-2.2.0/site/2.1/search.html +139 -0
- formative_ds-2.2.0/site/2.1/searchindex.js +1 -0
- formative_ds-2.2.0/site/versions.json +9 -0
- formative_ds-2.2.0/tests/test_policy.py +304 -0
- formative_ds-2.2.0/tests/test_policy_refutation.py +62 -0
- {formative_ds-2.1.0 → formative_ds-2.2.0}/uv.lock +1 -1
- formative_ds-2.2.0/vercel.json +15 -0
- formative_ds-2.1.0/Makefile +0 -18
- formative_ds-2.1.0/vercel.json +0 -5
- {formative_ds-2.1.0 → formative_ds-2.2.0}/.github/workflows/ci.yml +0 -0
- {formative_ds-2.1.0 → formative_ds-2.2.0}/.github/workflows/publish.yml +0 -0
- {formative_ds-2.1.0 → formative_ds-2.2.0}/.gitignore +0 -0
- {formative_ds-2.1.0 → formative_ds-2.2.0}/LICENSE +0 -0
- {formative_ds-2.1.0 → formative_ds-2.2.0}/docs/causal/benchmarks.rst +0 -0
- {formative_ds-2.1.0 → formative_ds-2.2.0}/docs/causal/dag.rst +0 -0
- {formative_ds-2.1.0 → formative_ds-2.2.0}/docs/causal/estimators.rst +0 -0
- {formative_ds-2.1.0 → formative_ds-2.2.0}/docs/causal/motivation.rst +0 -0
- {formative_ds-2.1.0 → formative_ds-2.2.0}/docs/causal/refutations.rst +0 -0
- {formative_ds-2.1.0 → formative_ds-2.2.0}/docs/causal/wizard.rst +0 -0
- {formative_ds-2.1.0 → formative_ds-2.2.0}/docs/game/decision_rules.rst +0 -0
- {formative_ds-2.1.0 → formative_ds-2.2.0}/docs/game/introduction.rst +0 -0
- {formative_ds-2.1.0 → formative_ds-2.2.0}/docs/game/single_player.rst +0 -0
- {formative_ds-2.1.0 → formative_ds-2.2.0}/docs/index.rst +0 -0
- {formative_ds-2.1.0 → formative_ds-2.2.0}/examples/decision/01_decision_basic.py +0 -0
- {formative_ds-2.1.0 → formative_ds-2.2.0}/examples/did/01_did_basic.py +0 -0
- {formative_ds-2.1.0 → formative_ds-2.2.0}/examples/iv/01_iv_basic.py +0 -0
- {formative_ds-2.1.0 → formative_ds-2.2.0}/examples/iv/02_iv_vs_ols_bias.py +0 -0
- {formative_ds-2.1.0 → formative_ds-2.2.0}/examples/iv/03_iv_with_observed_confounder.py +0 -0
- {formative_ds-2.1.0 → formative_ds-2.2.0}/examples/iv/04_iv_refutation.py +0 -0
- {formative_ds-2.1.0 → formative_ds-2.2.0}/examples/matching/01_basic_matching.py +0 -0
- {formative_ds-2.1.0 → formative_ds-2.2.0}/examples/matching/02_matching_vs_naive.py +0 -0
- {formative_ds-2.1.0 → formative_ds-2.2.0}/examples/matching/03_matching_refutation.py +0 -0
- {formative_ds-2.1.0 → formative_ds-2.2.0}/examples/ols/01_ols_basic.py +0 -0
- {formative_ds-2.1.0 → formative_ds-2.2.0}/examples/ols/02_ols_unobserved_confounder.py +0 -0
- {formative_ds-2.1.0 → formative_ds-2.2.0}/examples/ols/03_ols_multiple_confounders.py +0 -0
- {formative_ds-2.1.0 → formative_ds-2.2.0}/examples/rct/01_rct_basic.py +0 -0
- {formative_ds-2.1.0 → formative_ds-2.2.0}/examples/rdd/01_rdd_basic.py +0 -0
- {formative_ds-2.1.0 → formative_ds-2.2.0}/examples/rdd/02_rdd_positive_running_var.py +0 -0
- {formative_ds-2.1.0 → formative_ds-2.2.0}/formative/__init__.py +0 -0
- {formative_ds-2.1.0 → formative_ds-2.2.0}/formative/causal/_exceptions.py +0 -0
- {formative_ds-2.1.0 → formative_ds-2.2.0}/formative/causal/dag.py +0 -0
- {formative_ds-2.1.0 → formative_ds-2.2.0}/formative/causal/decision.py +0 -0
- {formative_ds-2.1.0 → formative_ds-2.2.0}/formative/causal/estimators/__init__.py +0 -0
- {formative_ds-2.1.0 → formative_ds-2.2.0}/formative/causal/estimators/_base.py +0 -0
- {formative_ds-2.1.0 → formative_ds-2.2.0}/formative/causal/estimators/_cate.py +0 -0
- {formative_ds-2.1.0 → formative_ds-2.2.0}/formative/causal/estimators/did.py +0 -0
- {formative_ds-2.1.0 → formative_ds-2.2.0}/formative/causal/estimators/iv.py +0 -0
- {formative_ds-2.1.0 → formative_ds-2.2.0}/formative/causal/estimators/matching.py +0 -0
- {formative_ds-2.1.0 → formative_ds-2.2.0}/formative/causal/estimators/ols.py +0 -0
- {formative_ds-2.1.0 → formative_ds-2.2.0}/formative/causal/estimators/rdd.py +0 -0
- {formative_ds-2.1.0 → formative_ds-2.2.0}/formative/causal/refutations/_check.py +0 -0
- {formative_ds-2.1.0 → formative_ds-2.2.0}/formative/causal/refutations/cate.py +0 -0
- {formative_ds-2.1.0 → formative_ds-2.2.0}/formative/causal/refutations/did.py +0 -0
- {formative_ds-2.1.0 → formative_ds-2.2.0}/formative/causal/refutations/iv.py +0 -0
- {formative_ds-2.1.0 → formative_ds-2.2.0}/formative/causal/refutations/matching.py +0 -0
- {formative_ds-2.1.0 → formative_ds-2.2.0}/formative/causal/refutations/ols.py +0 -0
- {formative_ds-2.1.0 → formative_ds-2.2.0}/formative/causal/refutations/rct.py +0 -0
- {formative_ds-2.1.0 → formative_ds-2.2.0}/formative/causal/refutations/rdd.py +0 -0
- {formative_ds-2.1.0 → formative_ds-2.2.0}/formative/game/__init__.py +0 -0
- {formative_ds-2.1.0 → formative_ds-2.2.0}/formative/game/expected_value.py +0 -0
- {formative_ds-2.1.0 → formative_ds-2.2.0}/formative/game/hurwicz.py +0 -0
- {formative_ds-2.1.0 → formative_ds-2.2.0}/formative/game/laplace.py +0 -0
- {formative_ds-2.1.0 → formative_ds-2.2.0}/formative/game/maximax.py +0 -0
- {formative_ds-2.1.0 → formative_ds-2.2.0}/formative/game/maximin.py +0 -0
- {formative_ds-2.1.0 → formative_ds-2.2.0}/formative/game/minimax.py +0 -0
- {formative_ds-2.1.0 → formative_ds-2.2.0}/notebooks/card_krueger_benchmark.ipynb +0 -0
- {formative_ds-2.1.0 → formative_ds-2.2.0}/notebooks/lalonde_benchmark.ipynb +0 -0
- /formative_ds-2.1.0/docs/causal/decisions.rst → /formative_ds-2.2.0/site/2.1/_sources/causal/decisions.rst.txt +0 -0
- /formative_ds-2.1.0/docs/causal/estimands.rst → /formative_ds-2.2.0/site/2.1/_sources/causal/estimands.rst.txt +0 -0
- /formative_ds-2.1.0/docs/causal/introduction.rst → /formative_ds-2.2.0/site/2.1/_sources/causal/introduction.rst.txt +0 -0
- {formative_ds-2.1.0 → formative_ds-2.2.0}/tests/test_cate.py +0 -0
- {formative_ds-2.1.0 → formative_ds-2.2.0}/tests/test_cate_refutation.py +0 -0
- {formative_ds-2.1.0 → formative_ds-2.2.0}/tests/test_dag.py +0 -0
- {formative_ds-2.1.0 → formative_ds-2.2.0}/tests/test_decision.py +0 -0
- {formative_ds-2.1.0 → formative_ds-2.2.0}/tests/test_did.py +0 -0
- {formative_ds-2.1.0 → formative_ds-2.2.0}/tests/test_did_refutation.py +0 -0
- {formative_ds-2.1.0 → formative_ds-2.2.0}/tests/test_game.py +0 -0
- {formative_ds-2.1.0 → formative_ds-2.2.0}/tests/test_iv.py +0 -0
- {formative_ds-2.1.0 → formative_ds-2.2.0}/tests/test_iv_refutation.py +0 -0
- {formative_ds-2.1.0 → formative_ds-2.2.0}/tests/test_matching.py +0 -0
- {formative_ds-2.1.0 → formative_ds-2.2.0}/tests/test_matching_refutation.py +0 -0
- {formative_ds-2.1.0 → formative_ds-2.2.0}/tests/test_ols.py +0 -0
- {formative_ds-2.1.0 → formative_ds-2.2.0}/tests/test_ols_refutation.py +0 -0
- {formative_ds-2.1.0 → formative_ds-2.2.0}/tests/test_rct.py +0 -0
- {formative_ds-2.1.0 → formative_ds-2.2.0}/tests/test_rct_refutation.py +0 -0
- {formative_ds-2.1.0 → formative_ds-2.2.0}/tests/test_rdd.py +0 -0
- {formative_ds-2.1.0 → formative_ds-2.2.0}/tests/test_rdd_refutation.py +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
site/** linguist-generated=true
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# CLAUDE.md
|
|
2
2
|
|
|
3
|
+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
4
|
+
|
|
3
5
|
## Commands
|
|
4
6
|
|
|
5
7
|
```bash
|
|
@@ -7,9 +9,18 @@ uv sync --dev # install dependencies
|
|
|
7
9
|
uv run pytest # run all tests
|
|
8
10
|
uv run pytest tests/test_iv.py # run one file
|
|
9
11
|
uv run pytest tests/test_iv.py::TestIV2SLSEstimation::test_iv_recovers_true_effect # run one test
|
|
10
|
-
|
|
12
|
+
make lint # ruff check (excludes notebooks/)
|
|
13
|
+
make fmt # ruff format
|
|
14
|
+
make check # lint + format check + tests with coverage (CI requires ≥80%)
|
|
15
|
+
make build-docs # sphinx with -W: warnings are errors in CI
|
|
11
16
|
```
|
|
12
17
|
|
|
18
|
+
CI (`.github/workflows/ci.yml`) runs lint, format check, tests with `--cov-fail-under=80`, and the `-W` docs build on every PR — `make check` plus `make build-docs` reproduces it locally.
|
|
19
|
+
|
|
20
|
+
Published on PyPI as `formative-ds`; the import name is `formative`. Releases: `make release BUMP=patch|minor|major` — bumps the version, rebuilds docs, snapshots them into `site/<major.minor>/` via `scripts/snapshot_docs.py`, and pushes with tags to trigger the PyPI publish workflow.
|
|
21
|
+
|
|
22
|
+
Docs are versioned: `site/` holds one committed HTML snapshot per minor version plus `versions.json`; Vercel serves it statically (no build). Unversioned paths (including `/`) are rewritten — not redirected — to the latest snapshot via the catch-all rewrite in `vercel.json`, whose destination the snapshot script bumps each release. Never add a `site/index.html` (it would shadow the rewrite at the root), never edit `site/` by hand, and don't run the snapshot script outside a release. The version dropdown (`docs/_static/version_switcher.js`) fetches `versions.json` from the site root at runtime so old snapshots list newer versions too.
|
|
23
|
+
|
|
13
24
|
---
|
|
14
25
|
|
|
15
26
|
## Package structure
|
|
@@ -46,18 +57,24 @@ Called as `result.refute(df)` — always pass the same `df` used for `fit()`. Re
|
|
|
46
57
|
- `_PLACEBO_TIME_SEED = 22222` — time permutation in DiD
|
|
47
58
|
- `_PLACEBO_MODIFIER_SEED = 77777` — modifier permutation in CATE checks
|
|
48
59
|
- `_RANDOM_MODIFIER_SEED = 31313` — noise modifier column in CATE checks
|
|
60
|
+
- `_PLACEBO_POLICY_SEED = 88888` — feature permutation in policy checks
|
|
61
|
+
- `_RANDOM_POLICY_MODIFIER_SEED = 46464` — noise feature column in policy checks
|
|
62
|
+
- `_FOLD_SEED = 13579` — cross-fitting fold assignment in policy learning (not a refutation seed)
|
|
49
63
|
- `_BOOTSTRAP_SEED = 42` — safe to reuse in matching's bootstrap (samples row indices, not independent values)
|
|
50
64
|
|
|
51
65
|
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.
|
|
52
66
|
|
|
53
67
|
**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
68
|
|
|
69
|
+
**Policy learning** — `RCTResult.learn_policy(df, modifiers=[...], cost=, benefit=, max_depth=1|2)` (design note: `docs/design/policy_learning.md`) does Athey–Wager doubly robust policy learning: cross-fitted AIPW scores, exhaustive shallow-tree search over discrete features, honest (out-of-fold) value reported as the advantage over the best constant policy. Returns `PolicyResult` (`rules`, `value`, `assign()`, `refute()`). Refutation checks test the honest *value*, not tree structure — in-sample search happily splits on noise within its depth budget, so structural checks would be flaky. Trees are simplified post-hoc by dropping splits that don't change the assignment (equal-value trees with redundant conditions can win the search on candidate order).
|
|
70
|
+
|
|
55
71
|
**Package layout:**
|
|
56
72
|
- `formative/causal/dag.py` — `DAG`, `_Node`
|
|
57
73
|
- `formative/causal/_exceptions.py` — `IdentificationError`, `GraphError`
|
|
58
74
|
- `formative/causal/estimators/_base.py` — `_BaseResult`, `_StatsmodelsResult` (shared result plumbing: effect stats via `_param`, `decide()`, assumptions footer)
|
|
59
75
|
- `formative/causal/estimators/_cate.py` — `GroupEffect`, `_fit_cate`, `_CATEResultMixin`, modifier validation (shared by OLS/RCT and the CATE refutations)
|
|
60
76
|
- `formative/causal/estimators/ols.py` — `OLSObservational`, `OLSResult`, `OLSCATEResult`
|
|
77
|
+
- `formative/causal/estimators/policy.py` — `PolicyNode`, `PolicyResult`, `_fit_policy` (used by refutations), `_learn_policy` (entry from `RCTResult.learn_policy`)
|
|
61
78
|
- `formative/causal/estimators/iv.py` — `IV2SLS`, `IVResult`
|
|
62
79
|
- `formative/causal/estimators/matching.py` — `PropensityScoreMatching`, `MatchingResult`; exports `_propensity_scores`, `_att_from_ps` for use by refutations
|
|
63
80
|
- `formative/causal/estimators/rct.py` — `RCT`, `RCTResult`, `RCTCATEResult`
|
|
@@ -68,6 +85,7 @@ Do not change `_RCC_SEED` or `_PLACEBO_SEED` to 42 — the generated noise colli
|
|
|
68
85
|
- `formative/causal/refutations/iv.py` — `IVRefutationReport`, `_check_first_stage_f`, `_check_random_common_cause`
|
|
69
86
|
- `formative/causal/refutations/matching.py` — `MatchingRefutationReport`, `_check_placebo_treatment`, `_check_random_common_cause`
|
|
70
87
|
- `formative/causal/refutations/rct.py` — `RCTRefutationReport`, `_check_random_common_cause`
|
|
88
|
+
- `formative/causal/refutations/policy.py` — `PolicyRefutationReport`, `_check_placebo_modifiers`, `_check_random_modifier` (import `_fit_policy` lazily — a module-level import is circular via `refutations/__init__`)
|
|
71
89
|
- `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)
|
|
72
90
|
- `formative/causal/refutations/did.py` — `DiDRefutationReport`, `_check_placebo_group`, `_check_random_common_cause`
|
|
73
91
|
- `formative/causal/refutations/rdd.py` — `RDDRefutationReport`, `_check_placebo_cutoff`, `_check_random_common_cause`
|
|
@@ -79,6 +97,8 @@ Do not change `_RCC_SEED` or `_PLACEBO_SEED` to 42 — the generated noise colli
|
|
|
79
97
|
|
|
80
98
|
**Bootstrap-heavy tests:** matching tests use `setup_class` (not `setup_method`) so `.fit()` runs once per class. Use N=1_000 — larger N widens bootstrap SE faster than NN noise shrinks, causing refutation checks to fail even on well-specified data.
|
|
81
99
|
|
|
100
|
+
**Benchmarks:** `notebooks/` holds Jupyter notebooks (LaLonde, Card–Krueger) that validate estimators against published results from famous studies; `docs/causal/benchmarks.rst` renders them as a docs page. Notebooks are excluded from ruff. Runnable per-estimator example scripts live in `examples/`.
|
|
101
|
+
|
|
82
102
|
**Planned estimators:** the goal is to cover all methods in the causal inference decision tree at https://www.maxpagels.com/prototypes/causal-wizard. Remaining: **Synthetic Control**.
|
|
83
103
|
|
|
84
104
|
---
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
.PHONY: build-docs test lint fmt check release
|
|
2
|
+
|
|
3
|
+
build-docs:
|
|
4
|
+
rm -rf docs/_build
|
|
5
|
+
uv run --group docs sphinx-build -W -b html docs docs/_build
|
|
6
|
+
|
|
7
|
+
test:
|
|
8
|
+
uv run pytest --cov=formative --cov-report=term-missing --cov-fail-under=80
|
|
9
|
+
|
|
10
|
+
lint:
|
|
11
|
+
uv run ruff check --exclude notebooks .
|
|
12
|
+
|
|
13
|
+
fmt:
|
|
14
|
+
uv run ruff format --exclude notebooks .
|
|
15
|
+
|
|
16
|
+
check: lint
|
|
17
|
+
uv run ruff format --check --exclude notebooks .
|
|
18
|
+
uv run pytest --cov=formative --cov-report=term-missing --cov-fail-under=80
|
|
19
|
+
|
|
20
|
+
release: # usage: make release BUMP=patch|minor|major
|
|
21
|
+
@test -n "$(BUMP)" || { echo "Usage: make release BUMP=patch|minor|major"; exit 1; }
|
|
22
|
+
@git diff --quiet && git diff --cached --quiet || { echo "Working tree not clean — commit or stash first."; exit 1; }
|
|
23
|
+
uv lock --check
|
|
24
|
+
uvx bump-my-version bump $(BUMP)
|
|
25
|
+
$(MAKE) build-docs
|
|
26
|
+
uv run python scripts/snapshot_docs.py
|
|
27
|
+
git add site vercel.json
|
|
28
|
+
git commit -m "Snapshot docs for $$(uv run python scripts/snapshot_docs.py --print-version)"
|
|
29
|
+
git push --follow-tags
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: formative-ds
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.2.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
|
|
@@ -47,23 +47,28 @@ Comprehensive documentation is available at [docs.getformative.dev](https://docs
|
|
|
47
47
|
|
|
48
48
|
### Causal estimation
|
|
49
49
|
|
|
50
|
+
Every analysis follows the same four steps: assume, estimate, refute, decide.
|
|
51
|
+
|
|
50
52
|
```python
|
|
51
53
|
from formative.causal import DAG, OLSObservational
|
|
52
54
|
|
|
55
|
+
# 1. Encode your causal assumptions as a DAG
|
|
53
56
|
dag = DAG()
|
|
54
57
|
dag.assume("ability").causes("education", "income")
|
|
55
58
|
dag.assume("education").causes("income")
|
|
56
59
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
treatment="education",
|
|
60
|
-
outcome="income"
|
|
61
|
-
).fit(df)
|
|
62
|
-
|
|
60
|
+
# 2. Estimate the causal effect
|
|
61
|
+
result = OLSObservational(dag, treatment="education", outcome="income").fit(df)
|
|
63
62
|
print(result.summary())
|
|
63
|
+
|
|
64
|
+
# 3. Refute: stress-test the result's assumptions
|
|
65
|
+
print(result.refute(df).summary())
|
|
66
|
+
|
|
67
|
+
# 4. Decide: is the treatment worth acting on?
|
|
68
|
+
print(result.decide(cost=8, benefit=15))
|
|
64
69
|
```
|
|
65
70
|
|
|
66
|
-
Confounders declared in the DAG are controlled for automatically. If a confounder is absent from the dataframe, an `IdentificationError` is raised before any estimation runs.
|
|
71
|
+
Confounders declared in the DAG are controlled for automatically. If a confounder is absent from the dataframe, an `IdentificationError` is raised before any estimation runs. Some estimators go further at step 4 — per-group decisions, or learning a treatment rule with `learn_policy()`.
|
|
67
72
|
|
|
68
73
|
### Decision rules
|
|
69
74
|
|
|
@@ -100,12 +105,16 @@ This creates a `.venv`, installs all dependencies, and installs the package in e
|
|
|
100
105
|
### Releasing a new version
|
|
101
106
|
|
|
102
107
|
```bash
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
git push --follow-tags # triggers publish to PyPI
|
|
108
|
+
make release BUMP=patch # 0.1.0 → 0.1.1 (bug fixes)
|
|
109
|
+
make release BUMP=minor # 0.1.0 → 0.2.0 (new features)
|
|
110
|
+
make release BUMP=major # 0.1.0 → 1.0.0 (breaking changes)
|
|
107
111
|
```
|
|
108
112
|
|
|
113
|
+
One command does everything: bumps the version in `pyproject.toml` and `uv.lock`
|
|
114
|
+
(commit + tag), builds the docs, snapshots them into `site/<major.minor>/` (the versioned
|
|
115
|
+
docs site Vercel serves statically), and pushes with tags — which triggers the publish to
|
|
116
|
+
PyPI. It refuses to run if the working tree is dirty or `uv.lock` is out of date.
|
|
117
|
+
|
|
109
118
|
### Running tests
|
|
110
119
|
|
|
111
120
|
```bash
|
|
@@ -20,23 +20,28 @@ Comprehensive documentation is available at [docs.getformative.dev](https://docs
|
|
|
20
20
|
|
|
21
21
|
### Causal estimation
|
|
22
22
|
|
|
23
|
+
Every analysis follows the same four steps: assume, estimate, refute, decide.
|
|
24
|
+
|
|
23
25
|
```python
|
|
24
26
|
from formative.causal import DAG, OLSObservational
|
|
25
27
|
|
|
28
|
+
# 1. Encode your causal assumptions as a DAG
|
|
26
29
|
dag = DAG()
|
|
27
30
|
dag.assume("ability").causes("education", "income")
|
|
28
31
|
dag.assume("education").causes("income")
|
|
29
32
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
treatment="education",
|
|
33
|
-
outcome="income"
|
|
34
|
-
).fit(df)
|
|
35
|
-
|
|
33
|
+
# 2. Estimate the causal effect
|
|
34
|
+
result = OLSObservational(dag, treatment="education", outcome="income").fit(df)
|
|
36
35
|
print(result.summary())
|
|
36
|
+
|
|
37
|
+
# 3. Refute: stress-test the result's assumptions
|
|
38
|
+
print(result.refute(df).summary())
|
|
39
|
+
|
|
40
|
+
# 4. Decide: is the treatment worth acting on?
|
|
41
|
+
print(result.decide(cost=8, benefit=15))
|
|
37
42
|
```
|
|
38
43
|
|
|
39
|
-
Confounders declared in the DAG are controlled for automatically. If a confounder is absent from the dataframe, an `IdentificationError` is raised before any estimation runs.
|
|
44
|
+
Confounders declared in the DAG are controlled for automatically. If a confounder is absent from the dataframe, an `IdentificationError` is raised before any estimation runs. Some estimators go further at step 4 — per-group decisions, or learning a treatment rule with `learn_policy()`.
|
|
40
45
|
|
|
41
46
|
### Decision rules
|
|
42
47
|
|
|
@@ -73,12 +78,16 @@ This creates a `.venv`, installs all dependencies, and installs the package in e
|
|
|
73
78
|
### Releasing a new version
|
|
74
79
|
|
|
75
80
|
```bash
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
git push --follow-tags # triggers publish to PyPI
|
|
81
|
+
make release BUMP=patch # 0.1.0 → 0.1.1 (bug fixes)
|
|
82
|
+
make release BUMP=minor # 0.1.0 → 0.2.0 (new features)
|
|
83
|
+
make release BUMP=major # 0.1.0 → 1.0.0 (breaking changes)
|
|
80
84
|
```
|
|
81
85
|
|
|
86
|
+
One command does everything: bumps the version in `pyproject.toml` and `uv.lock`
|
|
87
|
+
(commit + tag), builds the docs, snapshots them into `site/<major.minor>/` (the versioned
|
|
88
|
+
docs site Vercel serves statically), and pushes with tags — which triggers the publish to
|
|
89
|
+
PyPI. It refuses to run if the working tree is dirty or `uv.lock` is out of date.
|
|
90
|
+
|
|
82
91
|
### Running tests
|
|
83
92
|
|
|
84
93
|
```bash
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/* Version dropdown for the sidebar.
|
|
2
|
+
*
|
|
3
|
+
* Locates the site root from the current URL (pages live under
|
|
4
|
+
* /<major.minor>/, possibly below a subpath) and fetches versions.json from
|
|
5
|
+
* there at runtime, so even old, frozen snapshots list all versions published
|
|
6
|
+
* since. Fails silently when the file is unreachable (e.g. a local docs
|
|
7
|
+
* build opened directly), leaving the sidebar untouched.
|
|
8
|
+
*/
|
|
9
|
+
(function () {
|
|
10
|
+
function init() {
|
|
11
|
+
var match = window.location.pathname.match(/^(.*?)\/(\d+\.\d+)\//);
|
|
12
|
+
var root = match ? match[1] : "";
|
|
13
|
+
var current = match ? match[2] : null;
|
|
14
|
+
|
|
15
|
+
fetch(root + "/versions.json")
|
|
16
|
+
.then(function (r) {
|
|
17
|
+
return r.ok ? r.json() : null;
|
|
18
|
+
})
|
|
19
|
+
.then(function (data) {
|
|
20
|
+
if (!data || !data.versions || !data.versions.length) return;
|
|
21
|
+
var box = document.querySelector(".wy-side-nav-search");
|
|
22
|
+
if (!box) return;
|
|
23
|
+
|
|
24
|
+
var select = document.createElement("select");
|
|
25
|
+
select.className = "version-switcher";
|
|
26
|
+
select.setAttribute("aria-label", "Documentation version");
|
|
27
|
+
data.versions.forEach(function (v) {
|
|
28
|
+
var opt = document.createElement("option");
|
|
29
|
+
opt.value = root + v.url;
|
|
30
|
+
opt.textContent = "v" + v.version + (v.version === data.latest ? " (latest)" : "");
|
|
31
|
+
if (v.version === current) opt.selected = true;
|
|
32
|
+
select.appendChild(opt);
|
|
33
|
+
});
|
|
34
|
+
select.addEventListener("change", function () {
|
|
35
|
+
if (select.value) window.location.href = select.value;
|
|
36
|
+
});
|
|
37
|
+
box.appendChild(select);
|
|
38
|
+
})
|
|
39
|
+
.catch(function () {});
|
|
40
|
+
}
|
|
41
|
+
if (document.readyState === "loading") {
|
|
42
|
+
document.addEventListener("DOMContentLoaded", init);
|
|
43
|
+
} else {
|
|
44
|
+
init();
|
|
45
|
+
}
|
|
46
|
+
})();
|