formative-ds 2.2.2__tar.gz → 2.2.4__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 (300) hide show
  1. {formative_ds-2.2.2 → formative_ds-2.2.4}/PKG-INFO +3 -3
  2. formative_ds-2.2.4/TODO.md +30 -0
  3. {formative_ds-2.2.2 → formative_ds-2.2.4}/docs/causal/benchmarks.rst +22 -5
  4. {formative_ds-2.2.2 → formative_ds-2.2.4}/docs/causal/decisions.rst +69 -24
  5. {formative_ds-2.2.2 → formative_ds-2.2.4}/docs/causal/introduction.rst +10 -5
  6. {formative_ds-2.2.2 → formative_ds-2.2.4}/formative/causal/dag.py +1 -1
  7. {formative_ds-2.2.2 → formative_ds-2.2.4}/pyproject.toml +3 -3
  8. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.2/_sources/causal/benchmarks.rst.txt +22 -5
  9. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.2/_sources/causal/decisions.rst.txt +69 -24
  10. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.2/_sources/causal/introduction.rst.txt +10 -5
  11. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.2/_static/documentation_options.js +1 -1
  12. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.2/causal/benchmarks.html +25 -8
  13. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.2/causal/dag.html +6 -6
  14. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.2/causal/decisions.html +75 -30
  15. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.2/causal/estimands.html +3 -3
  16. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.2/causal/estimators.html +11 -11
  17. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.2/causal/introduction.html +16 -12
  18. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.2/causal/motivation.html +7 -7
  19. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.2/causal/refutations.html +3 -3
  20. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.2/causal/wizard.html +3 -3
  21. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.2/game/decision_rules.html +3 -3
  22. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.2/game/introduction.html +3 -3
  23. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.2/game/single_player.html +3 -3
  24. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.2/genindex.html +3 -3
  25. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.2/index.html +5 -5
  26. formative_ds-2.2.4/site/2.2/objects.inv +0 -0
  27. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.2/search.html +3 -3
  28. formative_ds-2.2.4/site/2.2/searchindex.js +1 -0
  29. {formative_ds-2.2.2 → formative_ds-2.2.4}/uv.lock +131 -130
  30. formative_ds-2.2.2/site/2.2/objects.inv +0 -0
  31. formative_ds-2.2.2/site/2.2/searchindex.js +0 -1
  32. {formative_ds-2.2.2 → formative_ds-2.2.4}/.gitattributes +0 -0
  33. {formative_ds-2.2.2 → formative_ds-2.2.4}/.github/workflows/ci.yml +0 -0
  34. {formative_ds-2.2.2 → formative_ds-2.2.4}/.github/workflows/publish.yml +0 -0
  35. {formative_ds-2.2.2 → formative_ds-2.2.4}/.gitignore +0 -0
  36. {formative_ds-2.2.2 → formative_ds-2.2.4}/CLAUDE.md +0 -0
  37. {formative_ds-2.2.2 → formative_ds-2.2.4}/LICENSE +0 -0
  38. {formative_ds-2.2.2 → formative_ds-2.2.4}/Makefile +0 -0
  39. {formative_ds-2.2.2 → formative_ds-2.2.4}/README.md +0 -0
  40. {formative_ds-2.2.2 → formative_ds-2.2.4}/docs/_static/version_switcher.css +0 -0
  41. {formative_ds-2.2.2 → formative_ds-2.2.4}/docs/_static/version_switcher.js +0 -0
  42. {formative_ds-2.2.2 → formative_ds-2.2.4}/docs/causal/dag.rst +0 -0
  43. {formative_ds-2.2.2 → formative_ds-2.2.4}/docs/causal/estimands.rst +0 -0
  44. {formative_ds-2.2.2 → formative_ds-2.2.4}/docs/causal/estimators.rst +0 -0
  45. {formative_ds-2.2.2 → formative_ds-2.2.4}/docs/causal/motivation.rst +0 -0
  46. {formative_ds-2.2.2 → formative_ds-2.2.4}/docs/causal/refutations.rst +0 -0
  47. {formative_ds-2.2.2 → formative_ds-2.2.4}/docs/causal/wizard.rst +0 -0
  48. {formative_ds-2.2.2 → formative_ds-2.2.4}/docs/conf.py +0 -0
  49. {formative_ds-2.2.2 → formative_ds-2.2.4}/docs/design/policy_learning.md +0 -0
  50. {formative_ds-2.2.2 → formative_ds-2.2.4}/docs/game/decision_rules.rst +0 -0
  51. {formative_ds-2.2.2 → formative_ds-2.2.4}/docs/game/introduction.rst +0 -0
  52. {formative_ds-2.2.2 → formative_ds-2.2.4}/docs/game/single_player.rst +0 -0
  53. {formative_ds-2.2.2 → formative_ds-2.2.4}/docs/index.rst +1 -1
  54. {formative_ds-2.2.2 → formative_ds-2.2.4}/examples/decision/01_decision_basic.py +0 -0
  55. {formative_ds-2.2.2 → formative_ds-2.2.4}/examples/did/01_did_basic.py +0 -0
  56. {formative_ds-2.2.2 → formative_ds-2.2.4}/examples/iv/01_iv_basic.py +0 -0
  57. {formative_ds-2.2.2 → formative_ds-2.2.4}/examples/iv/02_iv_vs_ols_bias.py +0 -0
  58. {formative_ds-2.2.2 → formative_ds-2.2.4}/examples/iv/03_iv_with_observed_confounder.py +0 -0
  59. {formative_ds-2.2.2 → formative_ds-2.2.4}/examples/iv/04_iv_refutation.py +0 -0
  60. {formative_ds-2.2.2 → formative_ds-2.2.4}/examples/matching/01_basic_matching.py +0 -0
  61. {formative_ds-2.2.2 → formative_ds-2.2.4}/examples/matching/02_matching_vs_naive.py +0 -0
  62. {formative_ds-2.2.2 → formative_ds-2.2.4}/examples/matching/03_matching_refutation.py +0 -0
  63. {formative_ds-2.2.2 → formative_ds-2.2.4}/examples/ols/01_ols_basic.py +0 -0
  64. {formative_ds-2.2.2 → formative_ds-2.2.4}/examples/ols/02_ols_unobserved_confounder.py +0 -0
  65. {formative_ds-2.2.2 → formative_ds-2.2.4}/examples/ols/03_ols_multiple_confounders.py +0 -0
  66. {formative_ds-2.2.2 → formative_ds-2.2.4}/examples/rct/01_rct_basic.py +0 -0
  67. {formative_ds-2.2.2 → formative_ds-2.2.4}/examples/rdd/01_rdd_basic.py +0 -0
  68. {formative_ds-2.2.2 → formative_ds-2.2.4}/examples/rdd/02_rdd_positive_running_var.py +0 -0
  69. {formative_ds-2.2.2 → formative_ds-2.2.4}/formative/__init__.py +0 -0
  70. {formative_ds-2.2.2 → formative_ds-2.2.4}/formative/causal/__init__.py +0 -0
  71. {formative_ds-2.2.2 → formative_ds-2.2.4}/formative/causal/_assumptions.py +0 -0
  72. {formative_ds-2.2.2 → formative_ds-2.2.4}/formative/causal/_exceptions.py +0 -0
  73. {formative_ds-2.2.2 → formative_ds-2.2.4}/formative/causal/_explain.py +0 -0
  74. {formative_ds-2.2.2 → formative_ds-2.2.4}/formative/causal/decision.py +0 -0
  75. {formative_ds-2.2.2 → formative_ds-2.2.4}/formative/causal/estimators/__init__.py +0 -0
  76. {formative_ds-2.2.2 → formative_ds-2.2.4}/formative/causal/estimators/_base.py +0 -0
  77. {formative_ds-2.2.2 → formative_ds-2.2.4}/formative/causal/estimators/_cate.py +0 -0
  78. {formative_ds-2.2.2 → formative_ds-2.2.4}/formative/causal/estimators/did.py +0 -0
  79. {formative_ds-2.2.2 → formative_ds-2.2.4}/formative/causal/estimators/iv.py +0 -0
  80. {formative_ds-2.2.2 → formative_ds-2.2.4}/formative/causal/estimators/matching.py +0 -0
  81. {formative_ds-2.2.2 → formative_ds-2.2.4}/formative/causal/estimators/ols.py +0 -0
  82. {formative_ds-2.2.2 → formative_ds-2.2.4}/formative/causal/estimators/policy.py +0 -0
  83. {formative_ds-2.2.2 → formative_ds-2.2.4}/formative/causal/estimators/rct.py +0 -0
  84. {formative_ds-2.2.2 → formative_ds-2.2.4}/formative/causal/estimators/rdd.py +0 -0
  85. {formative_ds-2.2.2 → formative_ds-2.2.4}/formative/causal/refutations/__init__.py +0 -0
  86. {formative_ds-2.2.2 → formative_ds-2.2.4}/formative/causal/refutations/_check.py +0 -0
  87. {formative_ds-2.2.2 → formative_ds-2.2.4}/formative/causal/refutations/cate.py +0 -0
  88. {formative_ds-2.2.2 → formative_ds-2.2.4}/formative/causal/refutations/did.py +0 -0
  89. {formative_ds-2.2.2 → formative_ds-2.2.4}/formative/causal/refutations/iv.py +0 -0
  90. {formative_ds-2.2.2 → formative_ds-2.2.4}/formative/causal/refutations/matching.py +0 -0
  91. {formative_ds-2.2.2 → formative_ds-2.2.4}/formative/causal/refutations/ols.py +0 -0
  92. {formative_ds-2.2.2 → formative_ds-2.2.4}/formative/causal/refutations/policy.py +0 -0
  93. {formative_ds-2.2.2 → formative_ds-2.2.4}/formative/causal/refutations/rct.py +0 -0
  94. {formative_ds-2.2.2 → formative_ds-2.2.4}/formative/causal/refutations/rdd.py +0 -0
  95. {formative_ds-2.2.2 → formative_ds-2.2.4}/formative/game/__init__.py +0 -0
  96. {formative_ds-2.2.2 → formative_ds-2.2.4}/formative/game/expected_value.py +0 -0
  97. {formative_ds-2.2.2 → formative_ds-2.2.4}/formative/game/hurwicz.py +0 -0
  98. {formative_ds-2.2.2 → formative_ds-2.2.4}/formative/game/laplace.py +0 -0
  99. {formative_ds-2.2.2 → formative_ds-2.2.4}/formative/game/maximax.py +0 -0
  100. {formative_ds-2.2.2 → formative_ds-2.2.4}/formative/game/maximin.py +0 -0
  101. {formative_ds-2.2.2 → formative_ds-2.2.4}/formative/game/minimax.py +0 -0
  102. {formative_ds-2.2.2 → formative_ds-2.2.4}/notebooks/card_krueger_benchmark.ipynb +0 -0
  103. {formative_ds-2.2.2 → formative_ds-2.2.4}/notebooks/lalonde_benchmark.ipynb +0 -0
  104. {formative_ds-2.2.2 → formative_ds-2.2.4}/scripts/snapshot_docs.py +0 -0
  105. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.1/_sources/causal/benchmarks.rst.txt +0 -0
  106. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.1/_sources/causal/dag.rst.txt +0 -0
  107. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.1/_sources/causal/decisions.rst.txt +0 -0
  108. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.1/_sources/causal/estimands.rst.txt +0 -0
  109. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.1/_sources/causal/estimators.rst.txt +0 -0
  110. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.1/_sources/causal/introduction.rst.txt +0 -0
  111. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.1/_sources/causal/motivation.rst.txt +0 -0
  112. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.1/_sources/causal/refutations.rst.txt +0 -0
  113. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.1/_sources/causal/wizard.rst.txt +0 -0
  114. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.1/_sources/game/decision_rules.rst.txt +0 -0
  115. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.1/_sources/game/introduction.rst.txt +0 -0
  116. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.1/_sources/game/single_player.rst.txt +0 -0
  117. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.1/_sources/index.rst.txt +0 -0
  118. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.1/_static/_sphinx_javascript_frameworks_compat.js +0 -0
  119. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.1/_static/base-stemmer.js +0 -0
  120. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.1/_static/basic.css +0 -0
  121. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.1/_static/check-solid.svg +0 -0
  122. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.1/_static/clipboard.min.js +0 -0
  123. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.1/_static/copy-button.svg +0 -0
  124. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.1/_static/copybutton.css +0 -0
  125. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.1/_static/copybutton.js +0 -0
  126. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.1/_static/copybutton_funcs.js +0 -0
  127. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.1/_static/css/badge_only.css +0 -0
  128. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.1/_static/css/fonts/Roboto-Slab-Bold.woff +0 -0
  129. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.1/_static/css/fonts/Roboto-Slab-Bold.woff2 +0 -0
  130. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.1/_static/css/fonts/Roboto-Slab-Regular.woff +0 -0
  131. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.1/_static/css/fonts/Roboto-Slab-Regular.woff2 +0 -0
  132. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.1/_static/css/fonts/fontawesome-webfont.eot +0 -0
  133. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.1/_static/css/fonts/fontawesome-webfont.svg +0 -0
  134. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.1/_static/css/fonts/fontawesome-webfont.ttf +0 -0
  135. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.1/_static/css/fonts/fontawesome-webfont.woff +0 -0
  136. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.1/_static/css/fonts/fontawesome-webfont.woff2 +0 -0
  137. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.1/_static/css/fonts/lato-bold-italic.woff +0 -0
  138. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.1/_static/css/fonts/lato-bold-italic.woff2 +0 -0
  139. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.1/_static/css/fonts/lato-bold.woff +0 -0
  140. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.1/_static/css/fonts/lato-bold.woff2 +0 -0
  141. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.1/_static/css/fonts/lato-normal-italic.woff +0 -0
  142. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.1/_static/css/fonts/lato-normal-italic.woff2 +0 -0
  143. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.1/_static/css/fonts/lato-normal.woff +0 -0
  144. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.1/_static/css/fonts/lato-normal.woff2 +0 -0
  145. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.1/_static/css/theme.css +0 -0
  146. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.1/_static/doctools.js +0 -0
  147. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.1/_static/documentation_options.js +0 -0
  148. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.1/_static/english-stemmer.js +0 -0
  149. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.1/_static/file.png +0 -0
  150. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.1/_static/fonts/Lato/lato-bold.eot +0 -0
  151. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.1/_static/fonts/Lato/lato-bold.ttf +0 -0
  152. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.1/_static/fonts/Lato/lato-bold.woff +0 -0
  153. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.1/_static/fonts/Lato/lato-bold.woff2 +0 -0
  154. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.1/_static/fonts/Lato/lato-bolditalic.eot +0 -0
  155. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.1/_static/fonts/Lato/lato-bolditalic.ttf +0 -0
  156. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.1/_static/fonts/Lato/lato-bolditalic.woff +0 -0
  157. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.1/_static/fonts/Lato/lato-bolditalic.woff2 +0 -0
  158. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.1/_static/fonts/Lato/lato-italic.eot +0 -0
  159. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.1/_static/fonts/Lato/lato-italic.ttf +0 -0
  160. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.1/_static/fonts/Lato/lato-italic.woff +0 -0
  161. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.1/_static/fonts/Lato/lato-italic.woff2 +0 -0
  162. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.1/_static/fonts/Lato/lato-regular.eot +0 -0
  163. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.1/_static/fonts/Lato/lato-regular.ttf +0 -0
  164. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.1/_static/fonts/Lato/lato-regular.woff +0 -0
  165. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.1/_static/fonts/Lato/lato-regular.woff2 +0 -0
  166. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.1/_static/fonts/RobotoSlab/roboto-slab-v7-bold.eot +0 -0
  167. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.1/_static/fonts/RobotoSlab/roboto-slab-v7-bold.ttf +0 -0
  168. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.1/_static/fonts/RobotoSlab/roboto-slab-v7-bold.woff +0 -0
  169. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.1/_static/fonts/RobotoSlab/roboto-slab-v7-bold.woff2 +0 -0
  170. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.1/_static/fonts/RobotoSlab/roboto-slab-v7-regular.eot +0 -0
  171. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.1/_static/fonts/RobotoSlab/roboto-slab-v7-regular.ttf +0 -0
  172. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.1/_static/fonts/RobotoSlab/roboto-slab-v7-regular.woff +0 -0
  173. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.1/_static/fonts/RobotoSlab/roboto-slab-v7-regular.woff2 +0 -0
  174. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.1/_static/jquery.js +0 -0
  175. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.1/_static/js/badge_only.js +0 -0
  176. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.1/_static/js/theme.js +0 -0
  177. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.1/_static/js/versions.js +0 -0
  178. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.1/_static/language_data.js +0 -0
  179. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.1/_static/minus.png +0 -0
  180. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.1/_static/plus.png +0 -0
  181. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.1/_static/pygments.css +0 -0
  182. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.1/_static/searchtools.js +0 -0
  183. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.1/_static/sphinx_highlight.js +0 -0
  184. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.1/_static/version_switcher.css +0 -0
  185. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.1/_static/version_switcher.js +0 -0
  186. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.1/causal/benchmarks.html +0 -0
  187. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.1/causal/dag.html +0 -0
  188. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.1/causal/decisions.html +0 -0
  189. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.1/causal/estimands.html +0 -0
  190. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.1/causal/estimators.html +0 -0
  191. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.1/causal/introduction.html +0 -0
  192. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.1/causal/motivation.html +0 -0
  193. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.1/causal/refutations.html +0 -0
  194. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.1/causal/wizard.html +0 -0
  195. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.1/game/decision_rules.html +0 -0
  196. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.1/game/introduction.html +0 -0
  197. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.1/game/single_player.html +0 -0
  198. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.1/genindex.html +0 -0
  199. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.1/index.html +0 -0
  200. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.1/objects.inv +0 -0
  201. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.1/search.html +0 -0
  202. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.1/searchindex.js +0 -0
  203. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.2/_sources/causal/dag.rst.txt +0 -0
  204. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.2/_sources/causal/estimands.rst.txt +0 -0
  205. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.2/_sources/causal/estimators.rst.txt +0 -0
  206. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.2/_sources/causal/motivation.rst.txt +0 -0
  207. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.2/_sources/causal/refutations.rst.txt +0 -0
  208. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.2/_sources/causal/wizard.rst.txt +0 -0
  209. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.2/_sources/game/decision_rules.rst.txt +0 -0
  210. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.2/_sources/game/introduction.rst.txt +0 -0
  211. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.2/_sources/game/single_player.rst.txt +0 -0
  212. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.2/_sources/index.rst.txt +1 -1
  213. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.2/_static/_sphinx_javascript_frameworks_compat.js +0 -0
  214. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.2/_static/base-stemmer.js +0 -0
  215. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.2/_static/basic.css +0 -0
  216. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.2/_static/check-solid.svg +0 -0
  217. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.2/_static/clipboard.min.js +0 -0
  218. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.2/_static/copy-button.svg +0 -0
  219. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.2/_static/copybutton.css +0 -0
  220. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.2/_static/copybutton.js +0 -0
  221. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.2/_static/copybutton_funcs.js +0 -0
  222. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.2/_static/css/badge_only.css +0 -0
  223. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.2/_static/css/fonts/Roboto-Slab-Bold.woff +0 -0
  224. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.2/_static/css/fonts/Roboto-Slab-Bold.woff2 +0 -0
  225. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.2/_static/css/fonts/Roboto-Slab-Regular.woff +0 -0
  226. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.2/_static/css/fonts/Roboto-Slab-Regular.woff2 +0 -0
  227. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.2/_static/css/fonts/fontawesome-webfont.eot +0 -0
  228. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.2/_static/css/fonts/fontawesome-webfont.svg +0 -0
  229. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.2/_static/css/fonts/fontawesome-webfont.ttf +0 -0
  230. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.2/_static/css/fonts/fontawesome-webfont.woff +0 -0
  231. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.2/_static/css/fonts/fontawesome-webfont.woff2 +0 -0
  232. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.2/_static/css/fonts/lato-bold-italic.woff +0 -0
  233. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.2/_static/css/fonts/lato-bold-italic.woff2 +0 -0
  234. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.2/_static/css/fonts/lato-bold.woff +0 -0
  235. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.2/_static/css/fonts/lato-bold.woff2 +0 -0
  236. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.2/_static/css/fonts/lato-normal-italic.woff +0 -0
  237. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.2/_static/css/fonts/lato-normal-italic.woff2 +0 -0
  238. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.2/_static/css/fonts/lato-normal.woff +0 -0
  239. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.2/_static/css/fonts/lato-normal.woff2 +0 -0
  240. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.2/_static/css/theme.css +0 -0
  241. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.2/_static/doctools.js +0 -0
  242. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.2/_static/english-stemmer.js +0 -0
  243. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.2/_static/file.png +0 -0
  244. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.2/_static/fonts/Lato/lato-bold.eot +0 -0
  245. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.2/_static/fonts/Lato/lato-bold.ttf +0 -0
  246. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.2/_static/fonts/Lato/lato-bold.woff +0 -0
  247. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.2/_static/fonts/Lato/lato-bold.woff2 +0 -0
  248. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.2/_static/fonts/Lato/lato-bolditalic.eot +0 -0
  249. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.2/_static/fonts/Lato/lato-bolditalic.ttf +0 -0
  250. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.2/_static/fonts/Lato/lato-bolditalic.woff +0 -0
  251. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.2/_static/fonts/Lato/lato-bolditalic.woff2 +0 -0
  252. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.2/_static/fonts/Lato/lato-italic.eot +0 -0
  253. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.2/_static/fonts/Lato/lato-italic.ttf +0 -0
  254. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.2/_static/fonts/Lato/lato-italic.woff +0 -0
  255. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.2/_static/fonts/Lato/lato-italic.woff2 +0 -0
  256. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.2/_static/fonts/Lato/lato-regular.eot +0 -0
  257. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.2/_static/fonts/Lato/lato-regular.ttf +0 -0
  258. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.2/_static/fonts/Lato/lato-regular.woff +0 -0
  259. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.2/_static/fonts/Lato/lato-regular.woff2 +0 -0
  260. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.2/_static/fonts/RobotoSlab/roboto-slab-v7-bold.eot +0 -0
  261. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.2/_static/fonts/RobotoSlab/roboto-slab-v7-bold.ttf +0 -0
  262. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.2/_static/fonts/RobotoSlab/roboto-slab-v7-bold.woff +0 -0
  263. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.2/_static/fonts/RobotoSlab/roboto-slab-v7-bold.woff2 +0 -0
  264. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.2/_static/fonts/RobotoSlab/roboto-slab-v7-regular.eot +0 -0
  265. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.2/_static/fonts/RobotoSlab/roboto-slab-v7-regular.ttf +0 -0
  266. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.2/_static/fonts/RobotoSlab/roboto-slab-v7-regular.woff +0 -0
  267. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.2/_static/fonts/RobotoSlab/roboto-slab-v7-regular.woff2 +0 -0
  268. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.2/_static/jquery.js +0 -0
  269. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.2/_static/js/badge_only.js +0 -0
  270. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.2/_static/js/theme.js +0 -0
  271. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.2/_static/js/versions.js +0 -0
  272. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.2/_static/language_data.js +0 -0
  273. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.2/_static/minus.png +0 -0
  274. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.2/_static/plus.png +0 -0
  275. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.2/_static/pygments.css +0 -0
  276. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.2/_static/searchtools.js +0 -0
  277. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.2/_static/sphinx_highlight.js +0 -0
  278. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.2/_static/version_switcher.css +0 -0
  279. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/2.2/_static/version_switcher.js +0 -0
  280. {formative_ds-2.2.2 → formative_ds-2.2.4}/site/versions.json +0 -0
  281. {formative_ds-2.2.2 → formative_ds-2.2.4}/tests/test_cate.py +0 -0
  282. {formative_ds-2.2.2 → formative_ds-2.2.4}/tests/test_cate_refutation.py +0 -0
  283. {formative_ds-2.2.2 → formative_ds-2.2.4}/tests/test_dag.py +0 -0
  284. {formative_ds-2.2.2 → formative_ds-2.2.4}/tests/test_decision.py +0 -0
  285. {formative_ds-2.2.2 → formative_ds-2.2.4}/tests/test_did.py +0 -0
  286. {formative_ds-2.2.2 → formative_ds-2.2.4}/tests/test_did_refutation.py +0 -0
  287. {formative_ds-2.2.2 → formative_ds-2.2.4}/tests/test_game.py +0 -0
  288. {formative_ds-2.2.2 → formative_ds-2.2.4}/tests/test_iv.py +0 -0
  289. {formative_ds-2.2.2 → formative_ds-2.2.4}/tests/test_iv_refutation.py +0 -0
  290. {formative_ds-2.2.2 → formative_ds-2.2.4}/tests/test_matching.py +0 -0
  291. {formative_ds-2.2.2 → formative_ds-2.2.4}/tests/test_matching_refutation.py +0 -0
  292. {formative_ds-2.2.2 → formative_ds-2.2.4}/tests/test_ols.py +0 -0
  293. {formative_ds-2.2.2 → formative_ds-2.2.4}/tests/test_ols_refutation.py +0 -0
  294. {formative_ds-2.2.2 → formative_ds-2.2.4}/tests/test_policy.py +0 -0
  295. {formative_ds-2.2.2 → formative_ds-2.2.4}/tests/test_policy_refutation.py +0 -0
  296. {formative_ds-2.2.2 → formative_ds-2.2.4}/tests/test_rct.py +0 -0
  297. {formative_ds-2.2.2 → formative_ds-2.2.4}/tests/test_rct_refutation.py +0 -0
  298. {formative_ds-2.2.2 → formative_ds-2.2.4}/tests/test_rdd.py +0 -0
  299. {formative_ds-2.2.2 → formative_ds-2.2.4}/tests/test_rdd_refutation.py +0 -0
  300. {formative_ds-2.2.2 → formative_ds-2.2.4}/vercel.json +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: formative-ds
3
- Version: 2.2.2
3
+ Version: 2.2.4
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
@@ -9,7 +9,7 @@ Author: Max Pagels
9
9
  License: MIT
10
10
  License-File: LICENSE
11
11
  Keywords: DAG,causal estimation,causal inference,econometrics,statistics
12
- Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Development Status :: 5 - Production/Stable
13
13
  Classifier: Intended Audience :: Science/Research
14
14
  Classifier: License :: OSI Approved :: MIT License
15
15
  Classifier: Programming Language :: Python :: 3
@@ -0,0 +1,30 @@
1
+ # TODO
2
+
3
+ ## Relax DAG validation for policy-learning features
4
+
5
+ `RCTResult.learn_policy()` currently validates candidate features with
6
+ `_validate_modifier_dag` (shared with CATE effect modifiers), which requires
7
+ each feature to be an **ancestor of the outcome**. That requirement is
8
+ stricter than the statistics need:
9
+
10
+ - In an RCT, treatment is independent of every pre-treatment variable, so the
11
+ doubly robust value estimate is honest for a policy conditioning on *any*
12
+ pre-treatment feature — causal, proxy, or noise.
13
+ - Proxy features (e.g. `income → zip`, `income → outcome`: zip causes nothing
14
+ but predicts who benefits) are valid and useful targeting features, yet are
15
+ rejected when the DAG is specified honestly.
16
+ - Noise features can't inflate the honest out-of-fold value, and the
17
+ placebo/random-modifier refutation checks already guard against them.
18
+
19
+ The **mediator exclusion must stay**: a descendant of the treatment is not
20
+ actionable at assignment time and conditioning on it creates selection bias.
21
+
22
+ Plan:
23
+
24
+ - Give policy learning its own validation: feature must be a DAG node and not
25
+ a descendant of the treatment; drop the ancestor-of-outcome requirement.
26
+ - Keep the stricter check for CATE effect modifiers, where the output is an
27
+ interpretive claim ("the effect differs by X") rather than just a rule and
28
+ its value.
29
+ - Update the corresponding passage in `docs/causal/decisions.rst` (~line 231)
30
+ and the `learn_policy` docstring in `formative/causal/estimators/rct.py`.
@@ -118,7 +118,9 @@ treatment/outcome relationship. The observational DAG declares this:
118
118
  obs_dag.assume(c).causes("treat", "re78")
119
119
  obs_dag.assume("treat").causes("re78")
120
120
 
121
- result = OLSObservational(obs_dag, treatment="treat", outcome="re78").fit(observational)
121
+ result = OLSObservational(
122
+ obs_dag, treatment="treat", outcome="re78"
123
+ ).fit(observational)
122
124
  print(result.summary())
123
125
 
124
126
  .. code-block:: text
@@ -156,7 +158,9 @@ each treated person only with the most similar controls:
156
158
 
157
159
  from formative.causal import PropensityScoreMatching
158
160
 
159
- result = PropensityScoreMatching(obs_dag, treatment="treat", outcome="re78").fit(observational)
161
+ result = PropensityScoreMatching(
162
+ obs_dag, treatment="treat", outcome="re78"
163
+ ).fit(observational)
160
164
  print(result.summary())
161
165
 
162
166
  .. code-block:: text
@@ -293,7 +297,10 @@ Table 3 reports four cell means; formative's DiD will be built from exactly thes
293
297
 
294
298
  for state, name in [(1, "NJ"), (0, "PA")]:
295
299
  sub = raw[raw.STATE == state]
296
- print(f"{name}: before {sub.fte_before.mean():.2f}, after {sub.fte_after.mean():.2f}")
300
+ print(
301
+ f"{name}: before {sub.fte_before.mean():.2f}, "
302
+ f"after {sub.fte_after.mean():.2f}"
303
+ )
297
304
 
298
305
  .. code-block:: text
299
306
 
@@ -317,8 +324,18 @@ binary group and time indicators:
317
324
 
318
325
  long = pd.concat(
319
326
  [
320
- pd.DataFrame({"store": raw.SHEET, "nj": raw.STATE, "after": 0, "fte": raw.fte_before}),
321
- pd.DataFrame({"store": raw.SHEET, "nj": raw.STATE, "after": 1, "fte": raw.fte_after}),
327
+ pd.DataFrame({
328
+ "store": raw.SHEET,
329
+ "nj": raw.STATE,
330
+ "after": 0,
331
+ "fte": raw.fte_before,
332
+ }),
333
+ pd.DataFrame({
334
+ "store": raw.SHEET,
335
+ "nj": raw.STATE,
336
+ "after": 1,
337
+ "fte": raw.fte_after,
338
+ }),
322
339
  ],
323
340
  ignore_index=True,
324
341
  ).dropna(subset=["fte"])
@@ -52,7 +52,11 @@ earnings using OLS with a DAG that encodes family background as a confounder:
52
52
  training = 0.6 * background + rng.normal(size=N)
53
53
  earnings = 3.0 * training + 1.2 * background + rng.normal(size=N)
54
54
 
55
- df = pd.DataFrame({"background": background, "training": training, "earnings": earnings})
55
+ df = pd.DataFrame({
56
+ "background": background,
57
+ "training": training,
58
+ "earnings": earnings,
59
+ })
56
60
  dag = DAG()
57
61
  dag.assume("background").causes("training", "earnings")
58
62
  dag.assume("training").causes("earnings")
@@ -85,7 +89,7 @@ But how confident are we in that estimate? And is it robust to estimation error?
85
89
  Decision confidence : 100.0%
86
90
  Robust to estimation error : Yes — decision is stable across 95% CI
87
91
 
88
- Game-theoretic robustness
92
+ Robustness
89
93
  ~~~~~~~~~~~~~~~~~~~~~~~~~
90
94
 
91
95
  ``decide()`` uses expected value maximisation: it picks "treat" when
@@ -96,7 +100,7 @@ treats a certain $37 gain and a 50/50 gamble between $0 and $74 identically.
96
100
  The ``robust`` flag is a first step toward robustness: it checks whether
97
101
  the decision flips anywhere inside the 95% confidence interval. But it
98
102
  only returns ``True`` or ``False``, and it is implicitly using the most
99
- conservative possible standard (the CI bounds).
103
+ conservative possible standard (the confidence interval bounds).
100
104
 
101
105
  For finer control, call ``to_outcomes()`` on the report and pass the result
102
106
  to any rule in ``formative.game``:
@@ -118,8 +122,8 @@ to any rule in ``formative.game``:
118
122
 
119
123
  By default, the three scenarios correspond to the 10th, 50th, and 90th
120
124
  percentiles of the net-benefit sampling distribution (assumed normal with
121
- the se derived from the 95% CI). The ``"don't treat"`` payoff is 0 in
122
- every scenario — the status quo baseline.
125
+ the standard error derived from the 95% confidence interval). The ``"don't treat"`` payoff is 0 in
126
+ every scenario.
123
127
 
124
128
  You can supply your own scenario names and quantiles:
125
129
 
@@ -129,17 +133,19 @@ You can supply your own scenario names and quantiles:
129
133
  scenarios={"bear": 0.05, "base": 0.50, "bull": 0.95}
130
134
  )
131
135
 
132
- **Relationship to** ``robust``. ``robust=True`` is equivalent to
133
- ``maximin`` returning the same choice as ``optimal`` when the scenarios are
134
- set to the CI bounds (quantiles 0.025 and 0.975). ``to_outcomes()``
136
+ How does this differ from using ``robust``? ``robust=True`` is equivalent to
137
+ ``maximin``, returning the same choice when the scenarios are
138
+ set to the confidence interval bounds (quantiles 0.025 and 0.975). ``to_outcomes()``
135
139
  generalises that check: different rules express different risk attitudes, and
136
140
  you can dial in your own pessimism level via ``hurwicz(alpha=...)``.
137
141
 
138
142
  Should we treat each segment?
139
143
  -----------------------------
140
144
 
141
- An average effect can hide groups where the treatment is worthless — or where
142
- it pays for itself several times over. When an estimator was fitted with an
145
+ An average effect can hide groups where the treatment is worthless, or where
146
+ it pays for itself several times over. It will also cause you to scale up a treatment
147
+ that is not worth it for everyone.
148
+ When an estimator is fitted with an
143
149
  ``effect_modifier`` (see :doc:`estimands`), ``decide_by_group(cost, benefit)``
144
150
  runs the same cost-benefit analysis within each level of the modifier and
145
151
  returns a mapping of level → :class:`~formative.DecisionReport`, so different
@@ -161,19 +167,33 @@ and everyone else not at all:
161
167
  tenure = rng.choice(["<2y", "2-5y", ">5y"], size=N)
162
168
  region = rng.choice(["north", "south"], size=N)
163
169
  coaching = rng.integers(0, 2, size=N)
164
- retention = 6.0 * coaching * (tenure == "<2y") + 2.0 * (tenure == ">5y") + rng.normal(size=N)
170
+ retention = (
171
+ 6.0 * coaching * (tenure == "<2y")
172
+ + 2.0 * (tenure == ">5y")
173
+ + rng.normal(size=N)
174
+ )
165
175
 
166
- df = pd.DataFrame({"tenure": tenure, "region": region, "coaching": coaching, "retention": retention})
176
+ df = pd.DataFrame({
177
+ "tenure": tenure,
178
+ "region": region,
179
+ "coaching": coaching,
180
+ "retention": retention,
181
+ })
167
182
 
168
183
  dag = DAG()
169
184
  dag.assume("coaching").causes("retention")
170
185
  dag.assume("tenure").causes("retention")
171
186
  dag.assume("region").causes("retention")
172
187
 
173
- result = RCT(dag, treatment="coaching", outcome="retention", effect_modifier="tenure").fit(df)
188
+ result = RCT(
189
+ dag, treatment="coaching", outcome="retention", effect_modifier="tenure"
190
+ ).fit(df)
174
191
  for level, decision in result.decide_by_group(cost=2.5, benefit=1.0).items():
175
192
  lo, hi = decision.net_benefit_ci
176
- print(f"{level:>5}: {decision.optimal:<12} net benefit {decision.net_benefit:+.2f} [{lo:+.2f}, {hi:+.2f}]")
193
+ print(
194
+ f"{level:>5}: {decision.optimal:<12} "
195
+ f"net benefit {decision.net_benefit:+.2f} [{lo:+.2f}, {hi:+.2f}]"
196
+ )
177
197
 
178
198
  .. code-block:: text
179
199
 
@@ -203,10 +223,9 @@ one less thing to get wrong.
203
223
  How it works
204
224
  ~~~~~~~~~~~~
205
225
 
206
- ``learn_policy()`` implements doubly robust policy learning in the style of
207
- Athey & Wager (2021):
226
+ ``learn_policy()`` implements doubly robust policy learning:
208
227
 
209
- 1. **Score every unit.** Each unit gets a cross-fitted AIPW score — an
228
+ 1. **Score every unit.** Each unit gets a cross-fitted score — an
210
229
  unbiased estimate of its individual treatment effect. Outcome models are
211
230
  fit by OLS on the candidate features over four folds and evaluated on the
212
231
  fifth, so no unit is scored by a model that saw it.
@@ -225,7 +244,7 @@ Athey & Wager (2021):
225
244
 
226
245
  Because the tree may split on any level of any candidate feature, features
227
246
  must be discrete: bin continuous columns before passing them in. Candidate
228
- features face the same DAG validation as effect modifiers — each must cause
247
+ features face the same DAG validation as effect modifiers; each must cause
229
248
  the outcome and must not be a descendant of the treatment (targeting on a
230
249
  mediator is not actionable at assignment time).
231
250
 
@@ -238,7 +257,13 @@ features instead of choosing a segmentation ourselves:
238
257
  .. code-block:: python
239
258
 
240
259
  result = RCT(dag, treatment="coaching", outcome="retention").fit(df)
241
- policy = result.learn_policy(df, modifiers=["tenure", "region"], cost=2.5, benefit=1.0, max_depth=2)
260
+ policy = result.learn_policy(
261
+ df,
262
+ modifiers=["tenure", "region"],
263
+ cost=2.5,
264
+ benefit=1.0,
265
+ max_depth=2
266
+ )
242
267
  print(policy.summary())
243
268
 
244
269
  .. code-block:: text
@@ -286,10 +311,17 @@ it only helps recent hires in the north:
286
311
  coaching = rng.integers(0, 2, size=N)
287
312
  helped = (tenure == "<2y") & (region == "north")
288
313
  retention = 7.0 * coaching * helped + 2.0 * (tenure == ">5y") + rng.normal(size=N)
289
- df = pd.DataFrame({"tenure": tenure, "region": region, "coaching": coaching, "retention": retention})
314
+ df = pd.DataFrame({
315
+ "tenure": tenure,
316
+ "region": region,
317
+ "coaching": coaching,
318
+ "retention": retention,
319
+ })
290
320
 
291
321
  result = RCT(dag, treatment="coaching", outcome="retention").fit(df)
292
- policy = result.learn_policy(df, modifiers=["tenure", "region"], cost=2.5, benefit=1.0, max_depth=2)
322
+ policy = result.learn_policy(
323
+ df, modifiers=["tenure", "region"], cost=2.5, benefit=1.0, max_depth=2
324
+ )
293
325
  print(policy.summary())
294
326
 
295
327
  .. code-block:: text
@@ -316,7 +348,9 @@ back to treating all recent hires and burns money on the southern half:
316
348
 
317
349
  .. code-block:: python
318
350
 
319
- result.learn_policy(df, modifiers=["tenure", "region"], cost=2.5, benefit=1.0, max_depth=1).value
351
+ result.learn_policy(
352
+ df, modifiers=["tenure", "region"], cost=2.5, benefit=1.0, max_depth=1
353
+ ).value
320
354
  # +0.34 per unit — versus +0.75 at depth 2
321
355
 
322
356
  When the deeper policy's value is clearly higher (as here), ship it; when the
@@ -339,7 +373,12 @@ segment-2 employees in the north:
339
373
  training = rng.integers(0, 2, size=N)
340
374
  effect = 4.0 * (segment == 0) + 5.0 * ((segment == 2) & (region == "north"))
341
375
  earnings = effect * training + 0.5 * segment + rng.normal(size=N)
342
- df = pd.DataFrame({"segment": segment, "region": region, "training": training, "earnings": earnings})
376
+ df = pd.DataFrame({
377
+ "segment": segment,
378
+ "region": region,
379
+ "training": training,
380
+ "earnings": earnings,
381
+ })
343
382
 
344
383
  dag = DAG()
345
384
  dag.assume("training").causes("earnings")
@@ -347,7 +386,13 @@ segment-2 employees in the north:
347
386
  dag.assume("region").causes("earnings")
348
387
 
349
388
  result = RCT(dag, treatment="training", outcome="earnings").fit(df)
350
- policy = result.learn_policy(df, modifiers=["segment", "region"], cost=1.0, benefit=1.0, max_depth=2)
389
+ policy = result.learn_policy(
390
+ df,
391
+ modifiers=["segment", "region"],
392
+ cost=1.0,
393
+ benefit=1.0,
394
+ max_depth=2
395
+ )
351
396
  print(policy.summary())
352
397
 
353
398
  .. code-block:: text
@@ -49,7 +49,7 @@ you choose.
49
49
 
50
50
  After you have obtained the result object, you can print a summary of the estimate and
51
51
  its assumptions. The assumptions are marked as testable or untestable depending on whether
52
- formative can check them. Most assumptions in causal inference are untestable by nature —
52
+ formative can check them. Most assumptions in causal inference are untestable by nature, i.e.
53
53
  things you must argue for based on domain knowledge and theory.
54
54
 
55
55
  .. code-block:: python
@@ -83,7 +83,7 @@ communicating with non-technical audiences.
83
83
  **3. Refute**
84
84
 
85
85
  Once you have a result, run statistical checks that probe whether its assumptions hold
86
- in the data. Each check returns a clear pass or fail — but no set of tests can guarantee
86
+ in the data. Each check returns a clear pass or fail, but no set of tests can guarantee
87
87
  validity. Use them as diagnostics, not proof. Causal inference is a judgment call, not a
88
88
  mathematical certainty.
89
89
 
@@ -104,10 +104,10 @@ mathematical certainty.
104
104
 
105
105
  **4. Decide**
106
106
 
107
- An estimate is rarely the end goal — the question is whether to act on it. Pass the
107
+ An estimate is rarely the end goal; the question is whether to act on it. Pass the
108
108
  cost per unit of treatment applied and the value of one unit of improvement in the
109
109
  outcome, and formative turns the estimate and its uncertainty into a treat/don't-treat
110
- call. Suppose a unit of education costs $8 and a unit of income is worth $15:
110
+ decision. Suppose a unit of education costs $8 and a unit of income is worth $15:
111
111
 
112
112
  .. code-block:: python
113
113
 
@@ -128,7 +128,12 @@ call. Suppose a unit of education costs $8 and a unit of income is worth $15:
128
128
  Decision confidence : 100.0%
129
129
  Robust to estimation error : Yes — decision is stable across 95% CI
130
130
 
131
- Depending on the estimator, decisions can go beyond a single call for everyone:
131
+ You may ask why a cost must be passed; is it not enough if the estimate is positive?
132
+ The answer is that a positive estimate does not guarantee that the treatment is worth it.
133
+ If the estimate gives an effect of +1.0, but the treatment costs $10 and the outcome is worth $1,
134
+ then the treatment is not worth it. The decision layer makes this explicit.
135
+
136
+ Depending on the estimator you choose, decisions can go beyond a single call for everyone:
132
137
  per-group decisions via an effect modifier, or a learned treatment rule that
133
138
  decides *whom* to treat. See :doc:`decisions` for the full decision layer.
134
139
 
@@ -34,7 +34,7 @@ class DAG:
34
34
  your data.
35
35
 
36
36
  Build the graph by calling ``assume().causes()`` for each causal relationship
37
- you believe holds. Include latent variables freely — if a node is not in
37
+ you believe holds. Include latent variables — if a node is not in
38
38
  your data at ``fit()`` time, the estimator treats it as unobserved.
39
39
 
40
40
  Example::
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "formative-ds"
7
- version = "2.2.2"
7
+ version = "2.2.4"
8
8
  description = "A Python library for quantitative reasoning."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -12,7 +12,7 @@ license = {text = "MIT"}
12
12
  authors = [{name = "Max Pagels"}]
13
13
  keywords = ["causal inference", "causal estimation", "econometrics", "statistics", "DAG"]
14
14
  classifiers = [
15
- "Development Status :: 3 - Alpha",
15
+ "Development Status :: 5 - Production/Stable",
16
16
  "Intended Audience :: Science/Research",
17
17
  "Topic :: Scientific/Engineering :: Mathematics",
18
18
  "License :: OSI Approved :: MIT License",
@@ -48,7 +48,7 @@ docs = ["sphinx>=7,<10", "sphinx-rtd-theme>=2,<4", "sphinx-copybutton>=0.5,<0.6"
48
48
  packages = ["formative"]
49
49
 
50
50
  [tool.bumpversion]
51
- current_version = "2.2.2"
51
+ current_version = "2.2.4"
52
52
  commit = true
53
53
  tag = true
54
54
  tag_name = "v{new_version}"
@@ -118,7 +118,9 @@ treatment/outcome relationship. The observational DAG declares this:
118
118
  obs_dag.assume(c).causes("treat", "re78")
119
119
  obs_dag.assume("treat").causes("re78")
120
120
 
121
- result = OLSObservational(obs_dag, treatment="treat", outcome="re78").fit(observational)
121
+ result = OLSObservational(
122
+ obs_dag, treatment="treat", outcome="re78"
123
+ ).fit(observational)
122
124
  print(result.summary())
123
125
 
124
126
  .. code-block:: text
@@ -156,7 +158,9 @@ each treated person only with the most similar controls:
156
158
 
157
159
  from formative.causal import PropensityScoreMatching
158
160
 
159
- result = PropensityScoreMatching(obs_dag, treatment="treat", outcome="re78").fit(observational)
161
+ result = PropensityScoreMatching(
162
+ obs_dag, treatment="treat", outcome="re78"
163
+ ).fit(observational)
160
164
  print(result.summary())
161
165
 
162
166
  .. code-block:: text
@@ -293,7 +297,10 @@ Table 3 reports four cell means; formative's DiD will be built from exactly thes
293
297
 
294
298
  for state, name in [(1, "NJ"), (0, "PA")]:
295
299
  sub = raw[raw.STATE == state]
296
- print(f"{name}: before {sub.fte_before.mean():.2f}, after {sub.fte_after.mean():.2f}")
300
+ print(
301
+ f"{name}: before {sub.fte_before.mean():.2f}, "
302
+ f"after {sub.fte_after.mean():.2f}"
303
+ )
297
304
 
298
305
  .. code-block:: text
299
306
 
@@ -317,8 +324,18 @@ binary group and time indicators:
317
324
 
318
325
  long = pd.concat(
319
326
  [
320
- pd.DataFrame({"store": raw.SHEET, "nj": raw.STATE, "after": 0, "fte": raw.fte_before}),
321
- pd.DataFrame({"store": raw.SHEET, "nj": raw.STATE, "after": 1, "fte": raw.fte_after}),
327
+ pd.DataFrame({
328
+ "store": raw.SHEET,
329
+ "nj": raw.STATE,
330
+ "after": 0,
331
+ "fte": raw.fte_before,
332
+ }),
333
+ pd.DataFrame({
334
+ "store": raw.SHEET,
335
+ "nj": raw.STATE,
336
+ "after": 1,
337
+ "fte": raw.fte_after,
338
+ }),
322
339
  ],
323
340
  ignore_index=True,
324
341
  ).dropna(subset=["fte"])
@@ -52,7 +52,11 @@ earnings using OLS with a DAG that encodes family background as a confounder:
52
52
  training = 0.6 * background + rng.normal(size=N)
53
53
  earnings = 3.0 * training + 1.2 * background + rng.normal(size=N)
54
54
 
55
- df = pd.DataFrame({"background": background, "training": training, "earnings": earnings})
55
+ df = pd.DataFrame({
56
+ "background": background,
57
+ "training": training,
58
+ "earnings": earnings,
59
+ })
56
60
  dag = DAG()
57
61
  dag.assume("background").causes("training", "earnings")
58
62
  dag.assume("training").causes("earnings")
@@ -85,7 +89,7 @@ But how confident are we in that estimate? And is it robust to estimation error?
85
89
  Decision confidence : 100.0%
86
90
  Robust to estimation error : Yes — decision is stable across 95% CI
87
91
 
88
- Game-theoretic robustness
92
+ Robustness
89
93
  ~~~~~~~~~~~~~~~~~~~~~~~~~
90
94
 
91
95
  ``decide()`` uses expected value maximisation: it picks "treat" when
@@ -96,7 +100,7 @@ treats a certain $37 gain and a 50/50 gamble between $0 and $74 identically.
96
100
  The ``robust`` flag is a first step toward robustness: it checks whether
97
101
  the decision flips anywhere inside the 95% confidence interval. But it
98
102
  only returns ``True`` or ``False``, and it is implicitly using the most
99
- conservative possible standard (the CI bounds).
103
+ conservative possible standard (the confidence interval bounds).
100
104
 
101
105
  For finer control, call ``to_outcomes()`` on the report and pass the result
102
106
  to any rule in ``formative.game``:
@@ -118,8 +122,8 @@ to any rule in ``formative.game``:
118
122
 
119
123
  By default, the three scenarios correspond to the 10th, 50th, and 90th
120
124
  percentiles of the net-benefit sampling distribution (assumed normal with
121
- the se derived from the 95% CI). The ``"don't treat"`` payoff is 0 in
122
- every scenario — the status quo baseline.
125
+ the standard error derived from the 95% confidence interval). The ``"don't treat"`` payoff is 0 in
126
+ every scenario.
123
127
 
124
128
  You can supply your own scenario names and quantiles:
125
129
 
@@ -129,17 +133,19 @@ You can supply your own scenario names and quantiles:
129
133
  scenarios={"bear": 0.05, "base": 0.50, "bull": 0.95}
130
134
  )
131
135
 
132
- **Relationship to** ``robust``. ``robust=True`` is equivalent to
133
- ``maximin`` returning the same choice as ``optimal`` when the scenarios are
134
- set to the CI bounds (quantiles 0.025 and 0.975). ``to_outcomes()``
136
+ How does this differ from using ``robust``? ``robust=True`` is equivalent to
137
+ ``maximin``, returning the same choice when the scenarios are
138
+ set to the confidence interval bounds (quantiles 0.025 and 0.975). ``to_outcomes()``
135
139
  generalises that check: different rules express different risk attitudes, and
136
140
  you can dial in your own pessimism level via ``hurwicz(alpha=...)``.
137
141
 
138
142
  Should we treat each segment?
139
143
  -----------------------------
140
144
 
141
- An average effect can hide groups where the treatment is worthless — or where
142
- it pays for itself several times over. When an estimator was fitted with an
145
+ An average effect can hide groups where the treatment is worthless, or where
146
+ it pays for itself several times over. It will also cause you to scale up a treatment
147
+ that is not worth it for everyone.
148
+ When an estimator is fitted with an
143
149
  ``effect_modifier`` (see :doc:`estimands`), ``decide_by_group(cost, benefit)``
144
150
  runs the same cost-benefit analysis within each level of the modifier and
145
151
  returns a mapping of level → :class:`~formative.DecisionReport`, so different
@@ -161,19 +167,33 @@ and everyone else not at all:
161
167
  tenure = rng.choice(["<2y", "2-5y", ">5y"], size=N)
162
168
  region = rng.choice(["north", "south"], size=N)
163
169
  coaching = rng.integers(0, 2, size=N)
164
- retention = 6.0 * coaching * (tenure == "<2y") + 2.0 * (tenure == ">5y") + rng.normal(size=N)
170
+ retention = (
171
+ 6.0 * coaching * (tenure == "<2y")
172
+ + 2.0 * (tenure == ">5y")
173
+ + rng.normal(size=N)
174
+ )
165
175
 
166
- df = pd.DataFrame({"tenure": tenure, "region": region, "coaching": coaching, "retention": retention})
176
+ df = pd.DataFrame({
177
+ "tenure": tenure,
178
+ "region": region,
179
+ "coaching": coaching,
180
+ "retention": retention,
181
+ })
167
182
 
168
183
  dag = DAG()
169
184
  dag.assume("coaching").causes("retention")
170
185
  dag.assume("tenure").causes("retention")
171
186
  dag.assume("region").causes("retention")
172
187
 
173
- result = RCT(dag, treatment="coaching", outcome="retention", effect_modifier="tenure").fit(df)
188
+ result = RCT(
189
+ dag, treatment="coaching", outcome="retention", effect_modifier="tenure"
190
+ ).fit(df)
174
191
  for level, decision in result.decide_by_group(cost=2.5, benefit=1.0).items():
175
192
  lo, hi = decision.net_benefit_ci
176
- print(f"{level:>5}: {decision.optimal:<12} net benefit {decision.net_benefit:+.2f} [{lo:+.2f}, {hi:+.2f}]")
193
+ print(
194
+ f"{level:>5}: {decision.optimal:<12} "
195
+ f"net benefit {decision.net_benefit:+.2f} [{lo:+.2f}, {hi:+.2f}]"
196
+ )
177
197
 
178
198
  .. code-block:: text
179
199
 
@@ -203,10 +223,9 @@ one less thing to get wrong.
203
223
  How it works
204
224
  ~~~~~~~~~~~~
205
225
 
206
- ``learn_policy()`` implements doubly robust policy learning in the style of
207
- Athey & Wager (2021):
226
+ ``learn_policy()`` implements doubly robust policy learning:
208
227
 
209
- 1. **Score every unit.** Each unit gets a cross-fitted AIPW score — an
228
+ 1. **Score every unit.** Each unit gets a cross-fitted score — an
210
229
  unbiased estimate of its individual treatment effect. Outcome models are
211
230
  fit by OLS on the candidate features over four folds and evaluated on the
212
231
  fifth, so no unit is scored by a model that saw it.
@@ -225,7 +244,7 @@ Athey & Wager (2021):
225
244
 
226
245
  Because the tree may split on any level of any candidate feature, features
227
246
  must be discrete: bin continuous columns before passing them in. Candidate
228
- features face the same DAG validation as effect modifiers — each must cause
247
+ features face the same DAG validation as effect modifiers; each must cause
229
248
  the outcome and must not be a descendant of the treatment (targeting on a
230
249
  mediator is not actionable at assignment time).
231
250
 
@@ -238,7 +257,13 @@ features instead of choosing a segmentation ourselves:
238
257
  .. code-block:: python
239
258
 
240
259
  result = RCT(dag, treatment="coaching", outcome="retention").fit(df)
241
- policy = result.learn_policy(df, modifiers=["tenure", "region"], cost=2.5, benefit=1.0, max_depth=2)
260
+ policy = result.learn_policy(
261
+ df,
262
+ modifiers=["tenure", "region"],
263
+ cost=2.5,
264
+ benefit=1.0,
265
+ max_depth=2
266
+ )
242
267
  print(policy.summary())
243
268
 
244
269
  .. code-block:: text
@@ -286,10 +311,17 @@ it only helps recent hires in the north:
286
311
  coaching = rng.integers(0, 2, size=N)
287
312
  helped = (tenure == "<2y") & (region == "north")
288
313
  retention = 7.0 * coaching * helped + 2.0 * (tenure == ">5y") + rng.normal(size=N)
289
- df = pd.DataFrame({"tenure": tenure, "region": region, "coaching": coaching, "retention": retention})
314
+ df = pd.DataFrame({
315
+ "tenure": tenure,
316
+ "region": region,
317
+ "coaching": coaching,
318
+ "retention": retention,
319
+ })
290
320
 
291
321
  result = RCT(dag, treatment="coaching", outcome="retention").fit(df)
292
- policy = result.learn_policy(df, modifiers=["tenure", "region"], cost=2.5, benefit=1.0, max_depth=2)
322
+ policy = result.learn_policy(
323
+ df, modifiers=["tenure", "region"], cost=2.5, benefit=1.0, max_depth=2
324
+ )
293
325
  print(policy.summary())
294
326
 
295
327
  .. code-block:: text
@@ -316,7 +348,9 @@ back to treating all recent hires and burns money on the southern half:
316
348
 
317
349
  .. code-block:: python
318
350
 
319
- result.learn_policy(df, modifiers=["tenure", "region"], cost=2.5, benefit=1.0, max_depth=1).value
351
+ result.learn_policy(
352
+ df, modifiers=["tenure", "region"], cost=2.5, benefit=1.0, max_depth=1
353
+ ).value
320
354
  # +0.34 per unit — versus +0.75 at depth 2
321
355
 
322
356
  When the deeper policy's value is clearly higher (as here), ship it; when the
@@ -339,7 +373,12 @@ segment-2 employees in the north:
339
373
  training = rng.integers(0, 2, size=N)
340
374
  effect = 4.0 * (segment == 0) + 5.0 * ((segment == 2) & (region == "north"))
341
375
  earnings = effect * training + 0.5 * segment + rng.normal(size=N)
342
- df = pd.DataFrame({"segment": segment, "region": region, "training": training, "earnings": earnings})
376
+ df = pd.DataFrame({
377
+ "segment": segment,
378
+ "region": region,
379
+ "training": training,
380
+ "earnings": earnings,
381
+ })
343
382
 
344
383
  dag = DAG()
345
384
  dag.assume("training").causes("earnings")
@@ -347,7 +386,13 @@ segment-2 employees in the north:
347
386
  dag.assume("region").causes("earnings")
348
387
 
349
388
  result = RCT(dag, treatment="training", outcome="earnings").fit(df)
350
- policy = result.learn_policy(df, modifiers=["segment", "region"], cost=1.0, benefit=1.0, max_depth=2)
389
+ policy = result.learn_policy(
390
+ df,
391
+ modifiers=["segment", "region"],
392
+ cost=1.0,
393
+ benefit=1.0,
394
+ max_depth=2
395
+ )
351
396
  print(policy.summary())
352
397
 
353
398
  .. code-block:: text