formative-ds 2.0.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.
Files changed (200) hide show
  1. formative_ds-2.2.0/.gitattributes +1 -0
  2. {formative_ds-2.0.0 → formative_ds-2.2.0}/.github/workflows/ci.yml +1 -1
  3. {formative_ds-2.0.0 → formative_ds-2.2.0}/CLAUDE.md +21 -1
  4. formative_ds-2.2.0/Makefile +29 -0
  5. {formative_ds-2.0.0 → formative_ds-2.2.0}/PKG-INFO +21 -12
  6. {formative_ds-2.0.0 → formative_ds-2.2.0}/README.md +20 -11
  7. formative_ds-2.2.0/docs/_static/version_switcher.css +11 -0
  8. formative_ds-2.2.0/docs/_static/version_switcher.js +46 -0
  9. formative_ds-2.2.0/docs/causal/benchmarks.rst +414 -0
  10. formative_ds-2.2.0/docs/causal/decisions.rst +458 -0
  11. formative_ds-2.2.0/docs/causal/estimands.rst +241 -0
  12. formative_ds-2.2.0/docs/causal/introduction.rst +178 -0
  13. {formative_ds-2.0.0 → formative_ds-2.2.0}/docs/conf.py +14 -0
  14. formative_ds-2.2.0/docs/design/policy_learning.md +128 -0
  15. {formative_ds-2.0.0 → formative_ds-2.2.0}/docs/index.rst +1 -0
  16. {formative_ds-2.0.0 → formative_ds-2.2.0}/formative/causal/__init__.py +5 -0
  17. {formative_ds-2.0.0 → formative_ds-2.2.0}/formative/causal/_explain.py +76 -0
  18. formative_ds-2.2.0/formative/causal/estimators/policy.py +491 -0
  19. {formative_ds-2.0.0 → formative_ds-2.2.0}/formative/causal/estimators/rct.py +45 -0
  20. {formative_ds-2.0.0 → formative_ds-2.2.0}/formative/causal/refutations/__init__.py +2 -0
  21. formative_ds-2.2.0/formative/causal/refutations/policy.py +117 -0
  22. formative_ds-2.2.0/notebooks/card_krueger_benchmark.ipynb +452 -0
  23. formative_ds-2.2.0/notebooks/lalonde_benchmark.ipynb +538 -0
  24. {formative_ds-2.0.0 → formative_ds-2.2.0}/pyproject.toml +15 -3
  25. formative_ds-2.2.0/scripts/snapshot_docs.py +72 -0
  26. formative_ds-2.2.0/site/2.1/_sources/causal/benchmarks.rst.txt +414 -0
  27. formative_ds-2.2.0/site/2.1/_sources/causal/dag.rst.txt +9 -0
  28. formative_ds-2.2.0/site/2.1/_sources/causal/estimators.rst.txt +100 -0
  29. formative_ds-2.2.0/site/2.1/_sources/causal/motivation.rst.txt +57 -0
  30. formative_ds-2.2.0/site/2.1/_sources/causal/refutations.rst.txt +54 -0
  31. formative_ds-2.2.0/site/2.1/_sources/causal/wizard.rst.txt +9 -0
  32. formative_ds-2.2.0/site/2.1/_sources/game/decision_rules.rst.txt +271 -0
  33. formative_ds-2.2.0/site/2.1/_sources/game/introduction.rst.txt +39 -0
  34. formative_ds-2.2.0/site/2.1/_sources/game/single_player.rst.txt +10 -0
  35. formative_ds-2.2.0/site/2.1/_sources/index.rst.txt +40 -0
  36. formative_ds-2.2.0/site/2.1/_static/_sphinx_javascript_frameworks_compat.js +123 -0
  37. formative_ds-2.2.0/site/2.1/_static/base-stemmer.js +476 -0
  38. formative_ds-2.2.0/site/2.1/_static/basic.css +906 -0
  39. formative_ds-2.2.0/site/2.1/_static/check-solid.svg +4 -0
  40. formative_ds-2.2.0/site/2.1/_static/clipboard.min.js +7 -0
  41. formative_ds-2.2.0/site/2.1/_static/copy-button.svg +5 -0
  42. formative_ds-2.2.0/site/2.1/_static/copybutton.css +94 -0
  43. formative_ds-2.2.0/site/2.1/_static/copybutton.js +248 -0
  44. formative_ds-2.2.0/site/2.1/_static/copybutton_funcs.js +73 -0
  45. formative_ds-2.2.0/site/2.1/_static/css/badge_only.css +1 -0
  46. formative_ds-2.2.0/site/2.1/_static/css/fonts/Roboto-Slab-Bold.woff +0 -0
  47. formative_ds-2.2.0/site/2.1/_static/css/fonts/Roboto-Slab-Bold.woff2 +0 -0
  48. formative_ds-2.2.0/site/2.1/_static/css/fonts/Roboto-Slab-Regular.woff +0 -0
  49. formative_ds-2.2.0/site/2.1/_static/css/fonts/Roboto-Slab-Regular.woff2 +0 -0
  50. formative_ds-2.2.0/site/2.1/_static/css/fonts/fontawesome-webfont.eot +0 -0
  51. formative_ds-2.2.0/site/2.1/_static/css/fonts/fontawesome-webfont.svg +2671 -0
  52. formative_ds-2.2.0/site/2.1/_static/css/fonts/fontawesome-webfont.ttf +0 -0
  53. formative_ds-2.2.0/site/2.1/_static/css/fonts/fontawesome-webfont.woff +0 -0
  54. formative_ds-2.2.0/site/2.1/_static/css/fonts/fontawesome-webfont.woff2 +0 -0
  55. formative_ds-2.2.0/site/2.1/_static/css/fonts/lato-bold-italic.woff +0 -0
  56. formative_ds-2.2.0/site/2.1/_static/css/fonts/lato-bold-italic.woff2 +0 -0
  57. formative_ds-2.2.0/site/2.1/_static/css/fonts/lato-bold.woff +0 -0
  58. formative_ds-2.2.0/site/2.1/_static/css/fonts/lato-bold.woff2 +0 -0
  59. formative_ds-2.2.0/site/2.1/_static/css/fonts/lato-normal-italic.woff +0 -0
  60. formative_ds-2.2.0/site/2.1/_static/css/fonts/lato-normal-italic.woff2 +0 -0
  61. formative_ds-2.2.0/site/2.1/_static/css/fonts/lato-normal.woff +0 -0
  62. formative_ds-2.2.0/site/2.1/_static/css/fonts/lato-normal.woff2 +0 -0
  63. formative_ds-2.2.0/site/2.1/_static/css/theme.css +4 -0
  64. formative_ds-2.2.0/site/2.1/_static/doctools.js +150 -0
  65. formative_ds-2.2.0/site/2.1/_static/documentation_options.js +13 -0
  66. formative_ds-2.2.0/site/2.1/_static/english-stemmer.js +1066 -0
  67. formative_ds-2.2.0/site/2.1/_static/file.png +0 -0
  68. formative_ds-2.2.0/site/2.1/_static/fonts/Lato/lato-bold.eot +0 -0
  69. formative_ds-2.2.0/site/2.1/_static/fonts/Lato/lato-bold.ttf +0 -0
  70. formative_ds-2.2.0/site/2.1/_static/fonts/Lato/lato-bold.woff +0 -0
  71. formative_ds-2.2.0/site/2.1/_static/fonts/Lato/lato-bold.woff2 +0 -0
  72. formative_ds-2.2.0/site/2.1/_static/fonts/Lato/lato-bolditalic.eot +0 -0
  73. formative_ds-2.2.0/site/2.1/_static/fonts/Lato/lato-bolditalic.ttf +0 -0
  74. formative_ds-2.2.0/site/2.1/_static/fonts/Lato/lato-bolditalic.woff +0 -0
  75. formative_ds-2.2.0/site/2.1/_static/fonts/Lato/lato-bolditalic.woff2 +0 -0
  76. formative_ds-2.2.0/site/2.1/_static/fonts/Lato/lato-italic.eot +0 -0
  77. formative_ds-2.2.0/site/2.1/_static/fonts/Lato/lato-italic.ttf +0 -0
  78. formative_ds-2.2.0/site/2.1/_static/fonts/Lato/lato-italic.woff +0 -0
  79. formative_ds-2.2.0/site/2.1/_static/fonts/Lato/lato-italic.woff2 +0 -0
  80. formative_ds-2.2.0/site/2.1/_static/fonts/Lato/lato-regular.eot +0 -0
  81. formative_ds-2.2.0/site/2.1/_static/fonts/Lato/lato-regular.ttf +0 -0
  82. formative_ds-2.2.0/site/2.1/_static/fonts/Lato/lato-regular.woff +0 -0
  83. formative_ds-2.2.0/site/2.1/_static/fonts/Lato/lato-regular.woff2 +0 -0
  84. formative_ds-2.2.0/site/2.1/_static/fonts/RobotoSlab/roboto-slab-v7-bold.eot +0 -0
  85. formative_ds-2.2.0/site/2.1/_static/fonts/RobotoSlab/roboto-slab-v7-bold.ttf +0 -0
  86. formative_ds-2.2.0/site/2.1/_static/fonts/RobotoSlab/roboto-slab-v7-bold.woff +0 -0
  87. formative_ds-2.2.0/site/2.1/_static/fonts/RobotoSlab/roboto-slab-v7-bold.woff2 +0 -0
  88. formative_ds-2.2.0/site/2.1/_static/fonts/RobotoSlab/roboto-slab-v7-regular.eot +0 -0
  89. formative_ds-2.2.0/site/2.1/_static/fonts/RobotoSlab/roboto-slab-v7-regular.ttf +0 -0
  90. formative_ds-2.2.0/site/2.1/_static/fonts/RobotoSlab/roboto-slab-v7-regular.woff +0 -0
  91. formative_ds-2.2.0/site/2.1/_static/fonts/RobotoSlab/roboto-slab-v7-regular.woff2 +0 -0
  92. formative_ds-2.2.0/site/2.1/_static/jquery.js +2 -0
  93. formative_ds-2.2.0/site/2.1/_static/js/badge_only.js +1 -0
  94. formative_ds-2.2.0/site/2.1/_static/js/theme.js +1 -0
  95. formative_ds-2.2.0/site/2.1/_static/js/versions.js +228 -0
  96. formative_ds-2.2.0/site/2.1/_static/language_data.js +13 -0
  97. formative_ds-2.2.0/site/2.1/_static/minus.png +0 -0
  98. formative_ds-2.2.0/site/2.1/_static/plus.png +0 -0
  99. formative_ds-2.2.0/site/2.1/_static/pygments.css +75 -0
  100. formative_ds-2.2.0/site/2.1/_static/searchtools.js +693 -0
  101. formative_ds-2.2.0/site/2.1/_static/sphinx_highlight.js +159 -0
  102. formative_ds-2.2.0/site/2.1/_static/version_switcher.css +11 -0
  103. formative_ds-2.2.0/site/2.1/_static/version_switcher.js +46 -0
  104. formative_ds-2.2.0/site/2.1/causal/benchmarks.html +547 -0
  105. formative_ds-2.2.0/site/2.1/causal/dag.html +296 -0
  106. formative_ds-2.2.0/site/2.1/causal/decisions.html +440 -0
  107. formative_ds-2.2.0/site/2.1/causal/estimands.html +344 -0
  108. formative_ds-2.2.0/site/2.1/causal/estimators.html +1957 -0
  109. formative_ds-2.2.0/site/2.1/causal/introduction.html +212 -0
  110. formative_ds-2.2.0/site/2.1/causal/motivation.html +175 -0
  111. formative_ds-2.2.0/site/2.1/causal/refutations.html +396 -0
  112. formative_ds-2.2.0/site/2.1/causal/wizard.html +133 -0
  113. formative_ds-2.2.0/site/2.1/game/decision_rules.html +892 -0
  114. formative_ds-2.2.0/site/2.1/game/introduction.html +156 -0
  115. formative_ds-2.2.0/site/2.1/game/single_player.html +135 -0
  116. formative_ds-2.2.0/site/2.1/genindex.html +754 -0
  117. formative_ds-2.2.0/site/2.1/index.html +140 -0
  118. formative_ds-2.2.0/site/2.1/objects.inv +0 -0
  119. formative_ds-2.2.0/site/2.1/search.html +139 -0
  120. formative_ds-2.2.0/site/2.1/searchindex.js +1 -0
  121. formative_ds-2.2.0/site/versions.json +9 -0
  122. formative_ds-2.2.0/tests/test_policy.py +304 -0
  123. formative_ds-2.2.0/tests/test_policy_refutation.py +62 -0
  124. {formative_ds-2.0.0 → formative_ds-2.2.0}/uv.lock +1624 -1
  125. formative_ds-2.2.0/vercel.json +15 -0
  126. formative_ds-2.0.0/Makefile +0 -18
  127. formative_ds-2.0.0/vercel.json +0 -5
  128. {formative_ds-2.0.0 → formative_ds-2.2.0}/.github/workflows/publish.yml +0 -0
  129. {formative_ds-2.0.0 → formative_ds-2.2.0}/.gitignore +0 -0
  130. {formative_ds-2.0.0 → formative_ds-2.2.0}/LICENSE +0 -0
  131. {formative_ds-2.0.0 → formative_ds-2.2.0}/docs/causal/dag.rst +0 -0
  132. {formative_ds-2.0.0 → formative_ds-2.2.0}/docs/causal/estimators.rst +0 -0
  133. {formative_ds-2.0.0 → formative_ds-2.2.0}/docs/causal/motivation.rst +0 -0
  134. {formative_ds-2.0.0 → formative_ds-2.2.0}/docs/causal/refutations.rst +0 -0
  135. {formative_ds-2.0.0 → formative_ds-2.2.0}/docs/causal/wizard.rst +0 -0
  136. {formative_ds-2.0.0 → formative_ds-2.2.0}/docs/game/decision_rules.rst +0 -0
  137. {formative_ds-2.0.0 → formative_ds-2.2.0}/docs/game/introduction.rst +0 -0
  138. {formative_ds-2.0.0 → formative_ds-2.2.0}/docs/game/single_player.rst +0 -0
  139. {formative_ds-2.0.0 → formative_ds-2.2.0}/examples/decision/01_decision_basic.py +0 -0
  140. {formative_ds-2.0.0 → formative_ds-2.2.0}/examples/did/01_did_basic.py +0 -0
  141. {formative_ds-2.0.0 → formative_ds-2.2.0}/examples/iv/01_iv_basic.py +0 -0
  142. {formative_ds-2.0.0 → formative_ds-2.2.0}/examples/iv/02_iv_vs_ols_bias.py +0 -0
  143. {formative_ds-2.0.0 → formative_ds-2.2.0}/examples/iv/03_iv_with_observed_confounder.py +0 -0
  144. {formative_ds-2.0.0 → formative_ds-2.2.0}/examples/iv/04_iv_refutation.py +0 -0
  145. {formative_ds-2.0.0 → formative_ds-2.2.0}/examples/matching/01_basic_matching.py +0 -0
  146. {formative_ds-2.0.0 → formative_ds-2.2.0}/examples/matching/02_matching_vs_naive.py +0 -0
  147. {formative_ds-2.0.0 → formative_ds-2.2.0}/examples/matching/03_matching_refutation.py +0 -0
  148. {formative_ds-2.0.0 → formative_ds-2.2.0}/examples/ols/01_ols_basic.py +0 -0
  149. {formative_ds-2.0.0 → formative_ds-2.2.0}/examples/ols/02_ols_unobserved_confounder.py +0 -0
  150. {formative_ds-2.0.0 → formative_ds-2.2.0}/examples/ols/03_ols_multiple_confounders.py +0 -0
  151. {formative_ds-2.0.0 → formative_ds-2.2.0}/examples/rct/01_rct_basic.py +0 -0
  152. {formative_ds-2.0.0 → formative_ds-2.2.0}/examples/rdd/01_rdd_basic.py +0 -0
  153. {formative_ds-2.0.0 → formative_ds-2.2.0}/examples/rdd/02_rdd_positive_running_var.py +0 -0
  154. {formative_ds-2.0.0 → formative_ds-2.2.0}/formative/__init__.py +0 -0
  155. {formative_ds-2.0.0 → formative_ds-2.2.0}/formative/causal/_exceptions.py +0 -0
  156. {formative_ds-2.0.0 → formative_ds-2.2.0}/formative/causal/dag.py +0 -0
  157. {formative_ds-2.0.0 → formative_ds-2.2.0}/formative/causal/decision.py +0 -0
  158. {formative_ds-2.0.0 → formative_ds-2.2.0}/formative/causal/estimators/__init__.py +0 -0
  159. {formative_ds-2.0.0 → formative_ds-2.2.0}/formative/causal/estimators/_base.py +0 -0
  160. {formative_ds-2.0.0 → formative_ds-2.2.0}/formative/causal/estimators/_cate.py +0 -0
  161. {formative_ds-2.0.0 → formative_ds-2.2.0}/formative/causal/estimators/did.py +0 -0
  162. {formative_ds-2.0.0 → formative_ds-2.2.0}/formative/causal/estimators/iv.py +0 -0
  163. {formative_ds-2.0.0 → formative_ds-2.2.0}/formative/causal/estimators/matching.py +0 -0
  164. {formative_ds-2.0.0 → formative_ds-2.2.0}/formative/causal/estimators/ols.py +0 -0
  165. {formative_ds-2.0.0 → formative_ds-2.2.0}/formative/causal/estimators/rdd.py +0 -0
  166. {formative_ds-2.0.0 → formative_ds-2.2.0}/formative/causal/refutations/_check.py +0 -0
  167. {formative_ds-2.0.0 → formative_ds-2.2.0}/formative/causal/refutations/cate.py +0 -0
  168. {formative_ds-2.0.0 → formative_ds-2.2.0}/formative/causal/refutations/did.py +0 -0
  169. {formative_ds-2.0.0 → formative_ds-2.2.0}/formative/causal/refutations/iv.py +0 -0
  170. {formative_ds-2.0.0 → formative_ds-2.2.0}/formative/causal/refutations/matching.py +0 -0
  171. {formative_ds-2.0.0 → formative_ds-2.2.0}/formative/causal/refutations/ols.py +0 -0
  172. {formative_ds-2.0.0 → formative_ds-2.2.0}/formative/causal/refutations/rct.py +0 -0
  173. {formative_ds-2.0.0 → formative_ds-2.2.0}/formative/causal/refutations/rdd.py +0 -0
  174. {formative_ds-2.0.0 → formative_ds-2.2.0}/formative/game/__init__.py +0 -0
  175. {formative_ds-2.0.0 → formative_ds-2.2.0}/formative/game/expected_value.py +0 -0
  176. {formative_ds-2.0.0 → formative_ds-2.2.0}/formative/game/hurwicz.py +0 -0
  177. {formative_ds-2.0.0 → formative_ds-2.2.0}/formative/game/laplace.py +0 -0
  178. {formative_ds-2.0.0 → formative_ds-2.2.0}/formative/game/maximax.py +0 -0
  179. {formative_ds-2.0.0 → formative_ds-2.2.0}/formative/game/maximin.py +0 -0
  180. {formative_ds-2.0.0 → formative_ds-2.2.0}/formative/game/minimax.py +0 -0
  181. /formative_ds-2.0.0/docs/causal/decisions.rst → /formative_ds-2.2.0/site/2.1/_sources/causal/decisions.rst.txt +0 -0
  182. /formative_ds-2.0.0/docs/causal/estimands.rst → /formative_ds-2.2.0/site/2.1/_sources/causal/estimands.rst.txt +0 -0
  183. /formative_ds-2.0.0/docs/causal/introduction.rst → /formative_ds-2.2.0/site/2.1/_sources/causal/introduction.rst.txt +0 -0
  184. {formative_ds-2.0.0 → formative_ds-2.2.0}/tests/test_cate.py +0 -0
  185. {formative_ds-2.0.0 → formative_ds-2.2.0}/tests/test_cate_refutation.py +0 -0
  186. {formative_ds-2.0.0 → formative_ds-2.2.0}/tests/test_dag.py +0 -0
  187. {formative_ds-2.0.0 → formative_ds-2.2.0}/tests/test_decision.py +0 -0
  188. {formative_ds-2.0.0 → formative_ds-2.2.0}/tests/test_did.py +0 -0
  189. {formative_ds-2.0.0 → formative_ds-2.2.0}/tests/test_did_refutation.py +0 -0
  190. {formative_ds-2.0.0 → formative_ds-2.2.0}/tests/test_game.py +0 -0
  191. {formative_ds-2.0.0 → formative_ds-2.2.0}/tests/test_iv.py +0 -0
  192. {formative_ds-2.0.0 → formative_ds-2.2.0}/tests/test_iv_refutation.py +0 -0
  193. {formative_ds-2.0.0 → formative_ds-2.2.0}/tests/test_matching.py +0 -0
  194. {formative_ds-2.0.0 → formative_ds-2.2.0}/tests/test_matching_refutation.py +0 -0
  195. {formative_ds-2.0.0 → formative_ds-2.2.0}/tests/test_ols.py +0 -0
  196. {formative_ds-2.0.0 → formative_ds-2.2.0}/tests/test_ols_refutation.py +0 -0
  197. {formative_ds-2.0.0 → formative_ds-2.2.0}/tests/test_rct.py +0 -0
  198. {formative_ds-2.0.0 → formative_ds-2.2.0}/tests/test_rct_refutation.py +0 -0
  199. {formative_ds-2.0.0 → formative_ds-2.2.0}/tests/test_rdd.py +0 -0
  200. {formative_ds-2.0.0 → formative_ds-2.2.0}/tests/test_rdd_refutation.py +0 -0
@@ -0,0 +1 @@
1
+ site/** linguist-generated=true
@@ -16,7 +16,7 @@ jobs:
16
16
  run: uv sync --dev --group docs
17
17
 
18
18
  - name: Lint
19
- run: uv run ruff check . && uv run ruff format --check .
19
+ run: uv run ruff check --exclude notebooks . && uv run ruff format --check --exclude notebooks .
20
20
 
21
21
  - name: Run tests
22
22
  run: uv run pytest --cov=formative --cov-report=term-missing --cov-fail-under=80
@@ -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
- uv run --group docs sphinx-build -b html docs docs/_build # build docs
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.0.0
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
- result = OLSObservational(
58
- dag,
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
- uvx bump-my-version bump patch # 0.1.0 → 0.1.1 (bug fixes)
104
- uvx bump-my-version bump minor # 0.1.0 → 0.2.0 (new features)
105
- uvx bump-my-version bump major # 0.1.0 → 1.0.0 (breaking changes)
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
- result = OLSObservational(
31
- dag,
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
- uvx bump-my-version bump patch # 0.1.0 → 0.1.1 (bug fixes)
77
- uvx bump-my-version bump minor # 0.1.0 → 0.2.0 (new features)
78
- uvx bump-my-version bump major # 0.1.0 → 1.0.0 (breaking changes)
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,11 @@
1
+ .wy-side-nav-search .version-switcher {
2
+ display: block;
3
+ width: 100%;
4
+ margin-top: 0.6em;
5
+ padding: 0.25em;
6
+ border: none;
7
+ border-radius: 3px;
8
+ font-size: 90%;
9
+ color: #404040;
10
+ background-color: #fcfcfc;
11
+ }
@@ -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
+ })();