ergodic 0.2.0__tar.gz → 0.2.1__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 (384) hide show
  1. {ergodic-0.2.0 → ergodic-0.2.1}/CHANGELOG.md +19 -3
  2. {ergodic-0.2.0 → ergodic-0.2.1}/PKG-INFO +1 -1
  3. {ergodic-0.2.0 → ergodic-0.2.1}/docsite/pages/compatibility.md +1 -1
  4. {ergodic-0.2.0 → ergodic-0.2.1}/docsite/pages/install.md +2 -2
  5. {ergodic-0.2.0 → ergodic-0.2.1}/docsite/pages/start.md +1 -1
  6. {ergodic-0.2.0 → ergodic-0.2.1}/docsite/pages/welt.md +10 -5
  7. {ergodic-0.2.0 → ergodic-0.2.1}/docsite/site.toml +1 -1
  8. {ergodic-0.2.0 → ergodic-0.2.1}/scripts/docsite/__main__.py +7 -1
  9. {ergodic-0.2.0 → ergodic-0.2.1}/scripts/docsite/links.py +10 -1
  10. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/__init__.py +1 -1
  11. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/process/_map.py +24 -3
  12. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/welt/_learner.py +45 -15
  13. {ergodic-0.2.0 → ergodic-0.2.1}/tests/test_docs_welt_page.py +37 -5
  14. {ergodic-0.2.0 → ergodic-0.2.1}/tests/test_docsite.py +6 -1
  15. {ergodic-0.2.0 → ergodic-0.2.1}/tests/test_process_map.py +11 -0
  16. {ergodic-0.2.0 → ergodic-0.2.1}/tests/test_welt.py +39 -3
  17. {ergodic-0.2.0 → ergodic-0.2.1}/.gitignore +0 -0
  18. {ergodic-0.2.0 → ergodic-0.2.1}/CONTRIBUTING.md +0 -0
  19. {ergodic-0.2.0 → ergodic-0.2.1}/LICENSE +0 -0
  20. {ergodic-0.2.0 → ergodic-0.2.1}/NOTICE +0 -0
  21. {ergodic-0.2.0 → ergodic-0.2.1}/README.md +0 -0
  22. {ergodic-0.2.0 → ergodic-0.2.1}/SECURITY.md +0 -0
  23. {ergodic-0.2.0 → ergodic-0.2.1}/docs/case-studies/01-birthweight.py +0 -0
  24. {ergodic-0.2.0 → ergodic-0.2.1}/docs/case-studies/02-berkeley.py +0 -0
  25. {ergodic-0.2.0 → ergodic-0.2.1}/docs/case-studies/03-pension401k.py +0 -0
  26. {ergodic-0.2.0 → ergodic-0.2.1}/docs/case-studies/04-synthetic-control.py +0 -0
  27. {ergodic-0.2.0 → ergodic-0.2.1}/docs/case-studies/05-discovery.py +0 -0
  28. {ergodic-0.2.0 → ergodic-0.2.1}/docs/case-studies/06-clv.py +0 -0
  29. {ergodic-0.2.0 → ergodic-0.2.1}/docs/case-studies/07-churn.py +0 -0
  30. {ergodic-0.2.0 → ergodic-0.2.1}/docs/learn/01-why-causality.py +0 -0
  31. {ergodic-0.2.0 → ergodic-0.2.1}/docs/learn/02-good-and-bad-controls.py +0 -0
  32. {ergodic-0.2.0 → ergodic-0.2.1}/docs/learn/03-estimating-effects.py +0 -0
  33. {ergodic-0.2.0 → ergodic-0.2.1}/docs/learn/04-heterogeneous-effects.py +0 -0
  34. {ergodic-0.2.0 → ergodic-0.2.1}/docs/learn/05-hidden-confounding.py +0 -0
  35. {ergodic-0.2.0 → ergodic-0.2.1}/docs/learn/06-discovering-structure.py +0 -0
  36. {ergodic-0.2.0 → ergodic-0.2.1}/docs/learn/07-quasi-experiments.py +0 -0
  37. {ergodic-0.2.0 → ergodic-0.2.1}/docs/learn/08-process-mining.py +0 -0
  38. {ergodic-0.2.0 → ergodic-0.2.1}/docs/learn/09-process-intelligence.py +0 -0
  39. {ergodic-0.2.0 → ergodic-0.2.1}/docs/learn/10-decision-points.py +0 -0
  40. {ergodic-0.2.0 → ergodic-0.2.1}/docs/learn/11-forecasting.py +0 -0
  41. {ergodic-0.2.0 → ergodic-0.2.1}/docs/learn/12-structural-causal-models.py +0 -0
  42. {ergodic-0.2.0 → ergodic-0.2.1}/docs/learn/13-hidden-regimes.py +0 -0
  43. {ergodic-0.2.0 → ergodic-0.2.1}/docs/learn/14-causal-forests-and-policy-trees.py +0 -0
  44. {ergodic-0.2.0 → ergodic-0.2.1}/docsite/assets/docs.css +0 -0
  45. {ergodic-0.2.0 → ergodic-0.2.1}/docsite/assets/docs.js +0 -0
  46. {ergodic-0.2.0 → ergodic-0.2.1}/docsite/assets/ergodic-mark-dark.svg +0 -0
  47. {ergodic-0.2.0 → ergodic-0.2.1}/docsite/assets/ergodic-mark-light.svg +0 -0
  48. {ergodic-0.2.0 → ergodic-0.2.1}/docsite/assets/fonts/OFL-Geist-Mono.txt +0 -0
  49. {ergodic-0.2.0 → ergodic-0.2.1}/docsite/assets/fonts/OFL-Geist.txt +0 -0
  50. {ergodic-0.2.0 → ergodic-0.2.1}/docsite/assets/fonts/geist-latin-ext-wght-normal.woff2 +0 -0
  51. {ergodic-0.2.0 → ergodic-0.2.1}/docsite/assets/fonts/geist-latin-wght-normal.woff2 +0 -0
  52. {ergodic-0.2.0 → ergodic-0.2.1}/docsite/assets/fonts/geist-mono-latin-ext-wght-normal.woff2 +0 -0
  53. {ergodic-0.2.0 → ergodic-0.2.1}/docsite/assets/fonts/geist-mono-latin-wght-normal.woff2 +0 -0
  54. {ergodic-0.2.0 → ergodic-0.2.1}/docsite/assets/site.css +0 -0
  55. {ergodic-0.2.0 → ergodic-0.2.1}/docsite/assets/tokens.css +0 -0
  56. {ergodic-0.2.0 → ergodic-0.2.1}/docsite/pages/adjustment-sets.md +0 -0
  57. {ergodic-0.2.0 → ergodic-0.2.1}/docsite/pages/assumptions.md +0 -0
  58. {ergodic-0.2.0 → ergodic-0.2.1}/docsite/pages/backtest-a-forecast.md +0 -0
  59. {ergodic-0.2.0 → ergodic-0.2.1}/docsite/pages/bootstrap-stability.md +0 -0
  60. {ergodic-0.2.0 → ergodic-0.2.1}/docsite/pages/certify-and-calibrate.md +0 -0
  61. {ergodic-0.2.0 → ergodic-0.2.1}/docsite/pages/changelog.md +0 -0
  62. {ergodic-0.2.0 → ergodic-0.2.1}/docsite/pages/choose-a-method.md +0 -0
  63. {ergodic-0.2.0 → ergodic-0.2.1}/docsite/pages/conformance-and-decisions.md +0 -0
  64. {ergodic-0.2.0 → ergodic-0.2.1}/docsite/pages/decode-a-graph.md +0 -0
  65. {ergodic-0.2.0 → ergodic-0.2.1}/docsite/pages/difference-in-differences.md +0 -0
  66. {ergodic-0.2.0 → ergodic-0.2.1}/docsite/pages/discover-a-graph.md +0 -0
  67. {ergodic-0.2.0 → ergodic-0.2.1}/docsite/pages/domain-knowledge.md +0 -0
  68. {ergodic-0.2.0 → ergodic-0.2.1}/docsite/pages/edge-probabilities.md +0 -0
  69. {ergodic-0.2.0 → ergodic-0.2.1}/docsite/pages/estimate-average-effect.md +0 -0
  70. {ergodic-0.2.0 → ergodic-0.2.1}/docsite/pages/fit-an-scm.md +0 -0
  71. {ergodic-0.2.0 → ergodic-0.2.1}/docsite/pages/forecast-with-drivers.md +0 -0
  72. {ergodic-0.2.0 → ergodic-0.2.1}/docsite/pages/glossary.md +0 -0
  73. {ergodic-0.2.0 → ergodic-0.2.1}/docsite/pages/graphs-and-marks.md +0 -0
  74. {ergodic-0.2.0 → ergodic-0.2.1}/docsite/pages/heterogeneous-effects.md +0 -0
  75. {ergodic-0.2.0 → ergodic-0.2.1}/docsite/pages/identify-an-effect.md +0 -0
  76. {ergodic-0.2.0 → ergodic-0.2.1}/docsite/pages/interventions-and-counterfactuals.md +0 -0
  77. {ergodic-0.2.0 → ergodic-0.2.1}/docsite/pages/map-a-process.md +0 -0
  78. {ergodic-0.2.0 → ergodic-0.2.1}/docsite/pages/mediation.md +0 -0
  79. {ergodic-0.2.0 → ergodic-0.2.1}/docsite/pages/reconcile-a-hierarchy.md +0 -0
  80. {ergodic-0.2.0 → ergodic-0.2.1}/docsite/pages/recover-hidden-regimes.md +0 -0
  81. {ergodic-0.2.0 → ergodic-0.2.1}/docsite/pages/refute-an-estimate.md +0 -0
  82. {ergodic-0.2.0 → ergodic-0.2.1}/docsite/pages/run-scenarios.md +0 -0
  83. {ergodic-0.2.0 → ergodic-0.2.1}/docsite/pages/synthetic-control.md +0 -0
  84. {ergodic-0.2.0 → ergodic-0.2.1}/docsite/pages/target-a-policy.md +0 -0
  85. {ergodic-0.2.0 → ergodic-0.2.1}/docsite/pages/time-series-discovery.md +0 -0
  86. {ergodic-0.2.0 → ergodic-0.2.1}/docsite/provenance.json +0 -0
  87. {ergodic-0.2.0 → ergodic-0.2.1}/examples/README.md +0 -0
  88. {ergodic-0.2.0 → ergodic-0.2.1}/examples/adjustment_birthweight.py +0 -0
  89. {ergodic-0.2.0 → ergodic-0.2.1}/examples/cate/01_cate_basics.py +0 -0
  90. {ergodic-0.2.0 → ergodic-0.2.1}/examples/cate/02_meta_learners_compared.py +0 -0
  91. {ergodic-0.2.0 → ergodic-0.2.1}/examples/cate/03_is_there_heterogeneity.py +0 -0
  92. {ergodic-0.2.0 → ergodic-0.2.1}/examples/cate/04_where_is_the_effect_higher.py +0 -0
  93. {ergodic-0.2.0 → ergodic-0.2.1}/examples/cate/05_targeting_with_rate.py +0 -0
  94. {ergodic-0.2.0 → ergodic-0.2.1}/examples/cate/06_per_unit_uncertainty.py +0 -0
  95. {ergodic-0.2.0 → ergodic-0.2.1}/examples/cate/07_cate_vs_ite.py +0 -0
  96. {ergodic-0.2.0 → ergodic-0.2.1}/examples/cate/08_identification_gating.py +0 -0
  97. {ergodic-0.2.0 → ergodic-0.2.1}/examples/cate/09_case_study_uplift_end_to_end.py +0 -0
  98. {ergodic-0.2.0 → ergodic-0.2.1}/examples/cate/10_causal_forest.py +0 -0
  99. {ergodic-0.2.0 → ergodic-0.2.1}/examples/cate/11_policy_trees.py +0 -0
  100. {ergodic-0.2.0 → ergodic-0.2.1}/examples/cate/12_model_selection.py +0 -0
  101. {ergodic-0.2.0 → ergodic-0.2.1}/examples/cate/13_case_study_phase2.py +0 -0
  102. {ergodic-0.2.0 → ergodic-0.2.1}/examples/cate/README.md +0 -0
  103. {ergodic-0.2.0 → ergodic-0.2.1}/examples/confounding_berkeley.py +0 -0
  104. {ergodic-0.2.0 → ergodic-0.2.1}/examples/discovery_asia.py +0 -0
  105. {ergodic-0.2.0 → ergodic-0.2.1}/examples/double_ml_pension.py +0 -0
  106. {ergodic-0.2.0 → ergodic-0.2.1}/examples/hidden_regimes.py +0 -0
  107. {ergodic-0.2.0 → ergodic-0.2.1}/examples/latent_switch.py +0 -0
  108. {ergodic-0.2.0 → ergodic-0.2.1}/examples/scm/01_three_rungs.py +0 -0
  109. {ergodic-0.2.0 → ergodic-0.2.1}/examples/scm/02_honesty_and_tipping_point.py +0 -0
  110. {ergodic-0.2.0 → ergodic-0.2.1}/examples/scm/03_causal_price_optimization.py +0 -0
  111. {ergodic-0.2.0 → ergodic-0.2.1}/examples/scm/04_counterfactual_recourse.py +0 -0
  112. {ergodic-0.2.0 → ergodic-0.2.1}/examples/scm/05_counterfactual_fairness.py +0 -0
  113. {ergodic-0.2.0 → ergodic-0.2.1}/examples/scm/06_causal_attribution.py +0 -0
  114. {ergodic-0.2.0 → ergodic-0.2.1}/examples/scm/07_but_for_accounting.py +0 -0
  115. {ergodic-0.2.0 → ergodic-0.2.1}/examples/scm/08_off_policy_evaluation.py +0 -0
  116. {ergodic-0.2.0 → ergodic-0.2.1}/examples/scm/09_root_cause_attribution.py +0 -0
  117. {ergodic-0.2.0 → ergodic-0.2.1}/examples/scm/10_frontdoor_unmeasured_confounder.py +0 -0
  118. {ergodic-0.2.0 → ergodic-0.2.1}/examples/scm/README.md +0 -0
  119. {ergodic-0.2.0 → ergodic-0.2.1}/examples/synthetic_control_smoking.py +0 -0
  120. {ergodic-0.2.0 → ergodic-0.2.1}/public/ergodic_logomark_dark.svg +0 -0
  121. {ergodic-0.2.0 → ergodic-0.2.1}/public/ergodic_logomark_light.svg +0 -0
  122. {ergodic-0.2.0 → ergodic-0.2.1}/pyproject.toml +0 -0
  123. {ergodic-0.2.0 → ergodic-0.2.1}/scripts/docsite/__init__.py +0 -0
  124. {ergodic-0.2.0 → ergodic-0.2.1}/scripts/docsite/build.py +0 -0
  125. {ergodic-0.2.0 → ergodic-0.2.1}/scripts/docsite/config.py +0 -0
  126. {ergodic-0.2.0 → ergodic-0.2.1}/scripts/docsite/examples.py +0 -0
  127. {ergodic-0.2.0 → ergodic-0.2.1}/scripts/docsite/graphite.py +0 -0
  128. {ergodic-0.2.0 → ergodic-0.2.1}/scripts/docsite/notebooks.py +0 -0
  129. {ergodic-0.2.0 → ergodic-0.2.1}/scripts/docsite/reference.py +0 -0
  130. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/data/__init__.py +0 -0
  131. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/data/_dataset.py +0 -0
  132. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/data/_eventlog.py +0 -0
  133. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/data/_frame.py +0 -0
  134. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/data/_hierarchy.py +0 -0
  135. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/data/_hts.py +0 -0
  136. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/data/_panel.py +0 -0
  137. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/data/_schema.py +0 -0
  138. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/data/_tabular.py +0 -0
  139. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/data/_timeseries.py +0 -0
  140. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/datasets/__init__.py +0 -0
  141. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/datasets/_bayesnet.py +0 -0
  142. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/datasets/_bunch.py +0 -0
  143. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/datasets/_bundled.py +0 -0
  144. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/datasets/_tables.py +0 -0
  145. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/datasets/data/birthweight.csv +0 -0
  146. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/datasets/data/germany.csv +0 -0
  147. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/datasets/data/pension401k.csv +0 -0
  148. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/datasets/data/smoking.csv +0 -0
  149. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/discovery/__init__.py +0 -0
  150. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/discovery/_bootstrap.py +0 -0
  151. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/discovery/_ci.py +0 -0
  152. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/discovery/_decode.py +0 -0
  153. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/discovery/_discover.py +0 -0
  154. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/discovery/_encode.py +0 -0
  155. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/discovery/_granger.py +0 -0
  156. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/discovery/_lagged.py +0 -0
  157. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/discovery/_probabilities.py +0 -0
  158. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/discovery/_registry.py +0 -0
  159. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/discovery/_result.py +0 -0
  160. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/discovery/_score.py +0 -0
  161. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/discovery/_stats.py +0 -0
  162. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/discovery/_tsresult.py +0 -0
  163. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/discovery/algorithms/__init__.py +0 -0
  164. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/discovery/algorithms/_astar.py +0 -0
  165. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/discovery/algorithms/_boss.py +0 -0
  166. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/discovery/algorithms/_fci.py +0 -0
  167. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/discovery/algorithms/_ges.py +0 -0
  168. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/discovery/algorithms/_glasso.py +0 -0
  169. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/discovery/algorithms/_grasp.py +0 -0
  170. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/discovery/algorithms/_hc.py +0 -0
  171. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/discovery/algorithms/_lingam.py +0 -0
  172. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/discovery/algorithms/_mmpc.py +0 -0
  173. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/discovery/algorithms/_pc.py +0 -0
  174. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/discovery/algorithms/_pcmci.py +0 -0
  175. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/discovery/algorithms/_rfci.py +0 -0
  176. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/discovery/algorithms/_skeleton.py +0 -0
  177. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/discovery/ci/__init__.py +0 -0
  178. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/discovery/ci/_discrete.py +0 -0
  179. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/discovery/ci/_gcm.py +0 -0
  180. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/discovery/ci/_linear.py +0 -0
  181. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/discovery/ci/_mixed.py +0 -0
  182. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/discovery/scores/__init__.py +0 -0
  183. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/discovery/scores/_bdeu.py +0 -0
  184. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/discovery/scores/_bic.py +0 -0
  185. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/discovery/scores/_discrete_bic.py +0 -0
  186. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/discovery/scores/_mixed_bic.py +0 -0
  187. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/forecast/__init__.py +0 -0
  188. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/forecast/_attribution.py +0 -0
  189. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/forecast/_backtest.py +0 -0
  190. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/forecast/_baselines.py +0 -0
  191. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/forecast/_facade.py +0 -0
  192. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/forecast/_fit.py +0 -0
  193. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/forecast/_mechanism.py +0 -0
  194. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/forecast/_metrics.py +0 -0
  195. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/forecast/_protocol.py +0 -0
  196. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/forecast/_reconcile.py +0 -0
  197. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/forecast/_result.py +0 -0
  198. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/forecast/_scalers.py +0 -0
  199. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/forecast/_schedule.py +0 -0
  200. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/forecast/_staged.py +0 -0
  201. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/forecast/_structure.py +0 -0
  202. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/forecast/mechanisms/__init__.py +0 -0
  203. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/forecast/mechanisms/_bass.py +0 -0
  204. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/forecast/mechanisms/_ou.py +0 -0
  205. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/forecast/mechanisms/_structural.py +0 -0
  206. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/graph/__init__.py +0 -0
  207. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/graph/_graph.py +0 -0
  208. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/graph/_io.py +0 -0
  209. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/graph/_marks.py +0 -0
  210. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/graph/_meek.py +0 -0
  211. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/graph/_pag.py +0 -0
  212. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/graph/_primitives.py +0 -0
  213. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/graph/_separation.py +0 -0
  214. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/graph/_surgery.py +0 -0
  215. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/identification/__init__.py +0 -0
  216. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/identification/_criteria.py +0 -0
  217. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/identification/_distribution.py +0 -0
  218. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/identification/_evaluate.py +0 -0
  219. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/identification/_functional.py +0 -0
  220. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/identification/_id.py +0 -0
  221. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/identification/_ida.py +0 -0
  222. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/identification/_mediation.py +0 -0
  223. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/identification/_recognize.py +0 -0
  224. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/identification/_record.py +0 -0
  225. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/inference/__init__.py +0 -0
  226. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/inference/_crossfit.py +0 -0
  227. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/inference/_design.py +0 -0
  228. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/inference/_estimate.py +0 -0
  229. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/inference/_estimator.py +0 -0
  230. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/inference/_learner.py +0 -0
  231. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/inference/_perunit.py +0 -0
  232. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/inference/_refute.py +0 -0
  233. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/inference/_registry.py +0 -0
  234. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/inference/_result.py +0 -0
  235. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/inference/_scores.py +0 -0
  236. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/inference/_select.py +0 -0
  237. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/inference/_stats.py +0 -0
  238. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/inference/_target.py +0 -0
  239. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/inference/estimators/__init__.py +0 -0
  240. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/inference/estimators/_did.py +0 -0
  241. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/inference/estimators/_dml.py +0 -0
  242. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/inference/estimators/_doubly_robust.py +0 -0
  243. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/inference/estimators/_forest.py +0 -0
  244. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/inference/estimators/_frontdoor.py +0 -0
  245. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/inference/estimators/_its.py +0 -0
  246. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/inference/estimators/_iv.py +0 -0
  247. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/inference/estimators/_mediation.py +0 -0
  248. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/inference/estimators/_meta.py +0 -0
  249. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/inference/estimators/_null.py +0 -0
  250. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/inference/estimators/_plugin.py +0 -0
  251. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/inference/estimators/_regression.py +0 -0
  252. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/inference/estimators/_synthetic_control.py +0 -0
  253. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/inference/estimators/_weighting.py +0 -0
  254. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/inference/explain/__init__.py +0 -0
  255. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/inference/explain/_blp.py +0 -0
  256. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/inference/explain/_calibration.py +0 -0
  257. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/inference/explain/_clan.py +0 -0
  258. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/inference/explain/_gates.py +0 -0
  259. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/inference/explain/_linalg.py +0 -0
  260. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/inference/explain/_policy.py +0 -0
  261. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/inference/explain/_rate.py +0 -0
  262. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/inference/explain/_record.py +0 -0
  263. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/inference/learners/__init__.py +0 -0
  264. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/inference/learners/_linear.py +0 -0
  265. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/inference/learners/_neighbors.py +0 -0
  266. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/inference/learners/_pymc.py +0 -0
  267. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/knowledge.py +0 -0
  268. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/latent/__init__.py +0 -0
  269. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/latent/_engine.py +0 -0
  270. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/latent/_facade.py +0 -0
  271. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/latent/_identify.py +0 -0
  272. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/latent/_likelihood.py +0 -0
  273. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/latent/_marginalize.py +0 -0
  274. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/latent/_result.py +0 -0
  275. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/latent/likelihoods/__init__.py +0 -0
  276. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/latent/likelihoods/_ar.py +0 -0
  277. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/latent/likelihoods/_spectral.py +0 -0
  278. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/process/__init__.py +0 -0
  279. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/process/_case_table.py +0 -0
  280. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/process/_cases.py +0 -0
  281. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/process/_conformance.py +0 -0
  282. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/process/_decisions.py +0 -0
  283. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/process/_filters.py +0 -0
  284. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/process/_mine.py +0 -0
  285. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/process/_net.py +0 -0
  286. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/process/_panel.py +0 -0
  287. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/process/_performance.py +0 -0
  288. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/process/_soundness.py +0 -0
  289. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/process/_tree.py +0 -0
  290. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/process/_variants.py +0 -0
  291. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/py.typed +0 -0
  292. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/scm/__init__.py +0 -0
  293. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/scm/_bounds.py +0 -0
  294. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/scm/_compose.py +0 -0
  295. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/scm/_district.py +0 -0
  296. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/scm/_mechanism.py +0 -0
  297. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/scm/_noise.py +0 -0
  298. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/scm/_result.py +0 -0
  299. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/scm/_sampling.py +0 -0
  300. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/scm/_scm.py +0 -0
  301. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/welt/__init__.py +0 -0
  302. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/welt/_client.py +0 -0
  303. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/welt/_data.py +0 -0
  304. {ergodic-0.2.0 → ergodic-0.2.1}/src/ergodic/welt/_discovery.py +0 -0
  305. {ergodic-0.2.0 → ergodic-0.2.1}/tests/conftest.py +0 -0
  306. {ergodic-0.2.0 → ergodic-0.2.1}/tests/data/exp0626_decoder_parity.json +0 -0
  307. {ergodic-0.2.0 → ergodic-0.2.1}/tests/test_bayes.py +0 -0
  308. {ergodic-0.2.0 → ergodic-0.2.1}/tests/test_case_table.py +0 -0
  309. {ergodic-0.2.0 → ergodic-0.2.1}/tests/test_cate.py +0 -0
  310. {ergodic-0.2.0 → ergodic-0.2.1}/tests/test_cate_forest.py +0 -0
  311. {ergodic-0.2.0 → ergodic-0.2.1}/tests/test_cate_phase2_stress.py +0 -0
  312. {ergodic-0.2.0 → ergodic-0.2.1}/tests/test_cate_policy.py +0 -0
  313. {ergodic-0.2.0 → ergodic-0.2.1}/tests/test_cate_select.py +0 -0
  314. {ergodic-0.2.0 → ergodic-0.2.1}/tests/test_cate_stress.py +0 -0
  315. {ergodic-0.2.0 → ergodic-0.2.1}/tests/test_conformance.py +0 -0
  316. {ergodic-0.2.0 → ergodic-0.2.1}/tests/test_data.py +0 -0
  317. {ergodic-0.2.0 → ergodic-0.2.1}/tests/test_datasets.py +0 -0
  318. {ergodic-0.2.0 → ergodic-0.2.1}/tests/test_decisions.py +0 -0
  319. {ergodic-0.2.0 → ergodic-0.2.1}/tests/test_decode.py +0 -0
  320. {ergodic-0.2.0 → ergodic-0.2.1}/tests/test_did.py +0 -0
  321. {ergodic-0.2.0 → ergodic-0.2.1}/tests/test_discovery.py +0 -0
  322. {ergodic-0.2.0 → ergodic-0.2.1}/tests/test_discovery_bootstrap.py +0 -0
  323. {ergodic-0.2.0 → ergodic-0.2.1}/tests/test_discovery_boss.py +0 -0
  324. {ergodic-0.2.0 → ergodic-0.2.1}/tests/test_discovery_ci.py +0 -0
  325. {ergodic-0.2.0 → ergodic-0.2.1}/tests/test_discovery_fci.py +0 -0
  326. {ergodic-0.2.0 → ergodic-0.2.1}/tests/test_discovery_ges.py +0 -0
  327. {ergodic-0.2.0 → ergodic-0.2.1}/tests/test_discovery_glasso.py +0 -0
  328. {ergodic-0.2.0 → ergodic-0.2.1}/tests/test_discovery_grasp.py +0 -0
  329. {ergodic-0.2.0 → ergodic-0.2.1}/tests/test_discovery_hc.py +0 -0
  330. {ergodic-0.2.0 → ergodic-0.2.1}/tests/test_discovery_lingam.py +0 -0
  331. {ergodic-0.2.0 → ergodic-0.2.1}/tests/test_discovery_mmpc.py +0 -0
  332. {ergodic-0.2.0 → ergodic-0.2.1}/tests/test_discovery_pc.py +0 -0
  333. {ergodic-0.2.0 → ergodic-0.2.1}/tests/test_discovery_pcmci.py +0 -0
  334. {ergodic-0.2.0 → ergodic-0.2.1}/tests/test_discovery_rfci.py +0 -0
  335. {ergodic-0.2.0 → ergodic-0.2.1}/tests/test_discovery_score.py +0 -0
  336. {ergodic-0.2.0 → ergodic-0.2.1}/tests/test_estimators_e2.py +0 -0
  337. {ergodic-0.2.0 → ergodic-0.2.1}/tests/test_forecast_backtest.py +0 -0
  338. {ergodic-0.2.0 → ergodic-0.2.1}/tests/test_forecast_backtest_mcmc.py +0 -0
  339. {ergodic-0.2.0 → ergodic-0.2.1}/tests/test_forecast_core.py +0 -0
  340. {ergodic-0.2.0 → ergodic-0.2.1}/tests/test_forecast_explain.py +0 -0
  341. {ergodic-0.2.0 → ergodic-0.2.1}/tests/test_forecast_facade.py +0 -0
  342. {ergodic-0.2.0 → ergodic-0.2.1}/tests/test_forecast_mechanisms.py +0 -0
  343. {ergodic-0.2.0 → ergodic-0.2.1}/tests/test_forecast_metrics.py +0 -0
  344. {ergodic-0.2.0 → ergodic-0.2.1}/tests/test_forecast_reconcile.py +0 -0
  345. {ergodic-0.2.0 → ergodic-0.2.1}/tests/test_forecast_staged.py +0 -0
  346. {ergodic-0.2.0 → ergodic-0.2.1}/tests/test_forecast_structure.py +0 -0
  347. {ergodic-0.2.0 → ergodic-0.2.1}/tests/test_forecast_zoo.py +0 -0
  348. {ergodic-0.2.0 → ergodic-0.2.1}/tests/test_granger.py +0 -0
  349. {ergodic-0.2.0 → ergodic-0.2.1}/tests/test_graph.py +0 -0
  350. {ergodic-0.2.0 → ergodic-0.2.1}/tests/test_graph_format.py +0 -0
  351. {ergodic-0.2.0 → ergodic-0.2.1}/tests/test_graph_primitives.py +0 -0
  352. {ergodic-0.2.0 → ergodic-0.2.1}/tests/test_id_algorithm.py +0 -0
  353. {ergodic-0.2.0 → ergodic-0.2.1}/tests/test_id_integration.py +0 -0
  354. {ergodic-0.2.0 → ergodic-0.2.1}/tests/test_identification.py +0 -0
  355. {ergodic-0.2.0 → ergodic-0.2.1}/tests/test_identification_classes.py +0 -0
  356. {ergodic-0.2.0 → ergodic-0.2.1}/tests/test_inductive_miner.py +0 -0
  357. {ergodic-0.2.0 → ergodic-0.2.1}/tests/test_inference.py +0 -0
  358. {ergodic-0.2.0 → ergodic-0.2.1}/tests/test_its.py +0 -0
  359. {ergodic-0.2.0 → ergodic-0.2.1}/tests/test_knowledge.py +0 -0
  360. {ergodic-0.2.0 → ergodic-0.2.1}/tests/test_kpi_panel.py +0 -0
  361. {ergodic-0.2.0 → ergodic-0.2.1}/tests/test_latent_calibrate.py +0 -0
  362. {ergodic-0.2.0 → ergodic-0.2.1}/tests/test_latent_certify.py +0 -0
  363. {ergodic-0.2.0 → ergodic-0.2.1}/tests/test_latent_facade.py +0 -0
  364. {ergodic-0.2.0 → ergodic-0.2.1}/tests/test_latent_golden.py +0 -0
  365. {ergodic-0.2.0 → ergodic-0.2.1}/tests/test_latent_identify.py +0 -0
  366. {ergodic-0.2.0 → ergodic-0.2.1}/tests/test_latent_marginalize.py +0 -0
  367. {ergodic-0.2.0 → ergodic-0.2.1}/tests/test_mediation.py +0 -0
  368. {ergodic-0.2.0 → ergodic-0.2.1}/tests/test_native_learners.py +0 -0
  369. {ergodic-0.2.0 → ergodic-0.2.1}/tests/test_pag.py +0 -0
  370. {ergodic-0.2.0 → ergodic-0.2.1}/tests/test_plugin.py +0 -0
  371. {ergodic-0.2.0 → ergodic-0.2.1}/tests/test_process_filters.py +0 -0
  372. {ergodic-0.2.0 → ergodic-0.2.1}/tests/test_refute.py +0 -0
  373. {ergodic-0.2.0 → ergodic-0.2.1}/tests/test_reprs.py +0 -0
  374. {ergodic-0.2.0 → ergodic-0.2.1}/tests/test_review_fixes.py +0 -0
  375. {ergodic-0.2.0 → ergodic-0.2.1}/tests/test_scm.py +0 -0
  376. {ergodic-0.2.0 → ergodic-0.2.1}/tests/test_scm_pymc.py +0 -0
  377. {ergodic-0.2.0 → ergodic-0.2.1}/tests/test_sharp_edges.py +0 -0
  378. {ergodic-0.2.0 → ergodic-0.2.1}/tests/test_sklearn_interop.py +0 -0
  379. {ergodic-0.2.0 → ergodic-0.2.1}/tests/test_smoke.py +0 -0
  380. {ergodic-0.2.0 → ergodic-0.2.1}/tests/test_synthetic_control.py +0 -0
  381. {ergodic-0.2.0 → ergodic-0.2.1}/tests/test_temporal_wiring.py +0 -0
  382. {ergodic-0.2.0 → ergodic-0.2.1}/tests/test_user_facing_wording.py +0 -0
  383. {ergodic-0.2.0 → ergodic-0.2.1}/tests/test_welt_isolation.py +0 -0
  384. {ergodic-0.2.0 → ergodic-0.2.1}/tests/test_workflow_net.py +0 -0
@@ -7,9 +7,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
- ## [0.2.0] - Unreleased
10
+ ## [0.2.1] - 2026-10-15
11
11
 
12
- To be released together with welt-client 0.13.
12
+ ### Fixed
13
+ - `WeltLearner` fits a slot with no feature columns locally. An empty adjustment set
14
+ leaves nothing for a model to learn, so the nuisance is the training mean (or the
15
+ class frequencies for classification): no upload, no paid fit, no charge to
16
+ `max_fits`, and `predictor_id_` is `None`. In 0.2.0 it raised, so the "Discover in
17
+ Welt, estimate in ergodic" guide stopped whenever Welt's Arrow found no confounders
18
+ for the treatment, as it does on the guide's own data today. With no adjustment
19
+ variables, DML is then the cross-fitted regression slope of the outcome on the
20
+ treatment, identical to DML with ergodic's `ols` learner.
21
+ - `ArcStats` treats two NaN gaps (START and END arcs) as equal and hashes them alike,
22
+ so `process_map(log) == process_map(log)` holds on Python 3.13 and 3.14, where
23
+ dataclass equality compares fields one by one.
24
+
25
+ ## [0.2.0] - 2026-10-15
26
+
27
+ Released together with welt-client 0.13.
13
28
 
14
29
  ### Added
15
30
  - A versioned graph format. `MixedGraph.to_dict()` writes
@@ -710,7 +725,8 @@ inference, discovery, process, SCM, and forecast pillars.
710
725
  silently with single-graph semantics; it now dispatches on the face, and
711
726
  mixed graphs raise instead of being misread.
712
727
 
713
- [Unreleased]: https://github.com/ergodic-ai/ergodic/compare/v0.2.0...HEAD
728
+ [Unreleased]: https://github.com/ergodic-ai/ergodic/compare/v0.2.1...HEAD
729
+ [0.2.1]: https://github.com/ergodic-ai/ergodic/compare/v0.2.0...v0.2.1
714
730
  [0.2.0]: https://github.com/ergodic-ai/ergodic/compare/v0.1.1...v0.2.0
715
731
  [0.1.1]: https://github.com/ergodic-ai/ergodic/compare/v0.1.0...v0.1.1
716
732
  [0.1.0]: https://github.com/ergodic-ai/ergodic/releases/tag/v0.1.0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: ergodic
3
- Version: 0.2.0
3
+ Version: 0.2.1
4
4
  Summary: Causal AI: causal discovery, causal inference, and process intelligence.
5
5
  Project-URL: Homepage, https://docs.ergodic.dev
6
6
  Project-URL: Documentation, https://docs.ergodic.dev
@@ -74,7 +74,7 @@ Only a graph that Welt declares a native DAG converts. `CausalResult.to_ergodic(
74
74
  in welt-client keeps every variable, unconnected ones included, and the native
75
75
  edge directions. It never projects, thresholds or relabels a graph into a DAG.
76
76
 
77
- | Welt model | Native graph | In ergodic 0.2.0 |
77
+ | Welt model | Native graph | In ergodic 0.2 |
78
78
  | --- | --- | --- |
79
79
  | `arrow` | DAG | Converts. `ergodic.welt.discover` returns it as a `DiscoveryResult`. |
80
80
  | `cdfm` | Directed, may hold cycles | Refused before paying by default. With an explicit `decoder=`, its scores are decoded locally into a DAG labelled decoded. |
@@ -72,7 +72,7 @@ for module, use in optional.items():
72
72
  ```
73
73
 
74
74
  ```output from="install_check.py" lines="1"
75
- ergodic 0.2.0 writes ergodic.graph/1
75
+ ergodic 0.2.1 writes ergodic.graph/1
76
76
  ```
77
77
 
78
78
  - **Step 1.** The version, and `ergodic.graph/1`: the format `MixedGraph.to_dict()`
@@ -104,7 +104,7 @@ xgboost yourself to use their models (the `welt` extra brings scikit-learn in).
104
104
 
105
105
  <details markdown="1"><summary>What are the neural and foundation extras?</summary>
106
106
 
107
- `neural` and `foundation` install PyTorch 2.2 or later. ergodic 0.2.0 has no
107
+ `neural` and `foundation` install PyTorch 2.2 or later. ergodic 0.2 has no
108
108
  learner that uses it: asking `cate` for a neural or foundation-model learner raises
109
109
  `NotImplementedError`.
110
110
 
@@ -95,6 +95,6 @@ treatment does not affect.
95
95
  - [Estimate an average effect](estimate-average-effect.md): adjust for known confounders on real data.
96
96
  - [Notebooks](notebooks.md): the learn series and case studies.
97
97
  - [Python reference](reference.md): every function and its parameters.
98
- - [Example scripts](https://github.com/ergodic-ai/ergodic/tree/v0.2.0/examples): runnable analyses of the bundled datasets.
98
+ - [Example scripts](https://github.com/ergodic-ai/ergodic/tree/v0.2.1/examples): runnable analyses of the bundled datasets.
99
99
 
100
100
  </div>
@@ -1,7 +1,8 @@
1
1
  # Discover in Welt, estimate in ergodic
2
2
 
3
3
  Learn a causal graph with a hosted Welt model, then identify and estimate an effect
4
- on it in ergodic. Welt also fits the nuisance models inside the estimate.
4
+ on it in ergodic. When the graph leaves variables to adjust for, Welt also fits the
5
+ nuisance models inside the estimate.
5
6
 
6
7
  !!! need "You'll need"
7
8
  - Python 3.11, 3.12 or 3.13.
@@ -72,8 +73,10 @@ depends on the Arrow version Welt serves. Each step prints:
72
73
  variables, so a directed path from `nprenatal` to `bweight` is identified by
73
74
  adjustment. The record names the adjustment set.
74
75
  - **Step 4.** An `EffectEstimate(dml, analytic: …)` line: the average change in
75
- grams per extra prenatal visit, its standard error and 95% interval. Then
76
- `paid fits: 4`.
76
+ grams per extra prenatal visit, its standard error and 95% interval. Then the
77
+ paid fits: 4 (two slots times two folds) when the adjustment set has variables,
78
+ or 0 when it is empty, because a slot with no features is just a mean and runs
79
+ locally.
77
80
 
78
81
  ## Read the provenance
79
82
 
@@ -215,8 +218,10 @@ With no directed path from `nprenatal` to `bweight`, `identify_effect` returns t
215
218
  `null` strategy: by the graph, the effect is zero. `strategy="dml"` then raises,
216
219
  because DML estimates adjustment effects only.
217
220
 
218
- With an empty adjustment set, DML's nuisance models have no features, and
219
- `WeltLearner` raises before it fits. Leave those slots to the default learner.
221
+ With an empty adjustment set, DML's nuisance models have no features: each one is
222
+ just the training mean. `WeltLearner` fits that locally, with no upload and no paid
223
+ fit, so the program prints `paid fits: 0` and the estimate is the regression slope
224
+ of `bweight` on `nprenatal` across folds.
220
225
 
221
226
  </details>
222
227
 
@@ -31,7 +31,7 @@ commands = [
31
31
 
32
32
  [versions]
33
33
  # Published versions older than this build that stay in the picker (v<version>/), oldest first.
34
- previous = []
34
+ previous = ["0.2.0"]
35
35
 
36
36
  [[products]]
37
37
  name = "ergodic"
@@ -36,6 +36,11 @@ def main(argv: list[str] | None = None) -> int:
36
36
  )
37
37
  links = commands.add_parser("links", help="check internal links of a built site")
38
38
  links.add_argument("output", type=Path, nargs="?", default=ROOT / "site")
39
+ links.add_argument(
40
+ "--retained-elsewhere",
41
+ action="store_true",
42
+ help="count links into the retained versions (site.toml previous) as present",
43
+ )
39
44
  notebooks = commands.add_parser("notebooks", help="execute every notebook (release CI)")
40
45
  notebooks.add_argument("--timeout", type=int, default=1800)
41
46
  notebooks.add_argument("--workers", type=int, default=1)
@@ -93,7 +98,8 @@ def main(argv: list[str] | None = None) -> int:
93
98
  if args.command == "links":
94
99
  from .links import check_links
95
100
 
96
- problems = check_links(args.output)
101
+ elsewhere = load_site().previous_versions if args.retained_elsewhere else ()
102
+ problems = check_links(args.output, elsewhere)
97
103
  for problem in problems:
98
104
  print(problem, file=sys.stderr)
99
105
  print(f"Links: {len(problems)} problem(s) in {args.output}")
@@ -22,9 +22,16 @@ def target_file(base: Path, reference: str) -> Path:
22
22
  return target / "index.html" if target.is_dir() else target
23
23
 
24
24
 
25
- def check_links(output: Path) -> list[str]:
25
+ def check_links(output: Path, elsewhere: tuple[str, ...] = ()) -> list[str]:
26
+ """Problems with the site's internal links, assets and anchors.
27
+
28
+ ``elsewhere`` names version folders (``"0.2.0"`` for ``v0.2.0/``) that another
29
+ build publishes next to this one: links into them count as present. The full
30
+ published site is checked without it.
31
+ """
26
32
  problems = []
27
33
  root = output.resolve()
34
+ retained = [root / f"v{version}" for version in elsewhere]
28
35
  for page in sorted(root.rglob("*.html")):
29
36
  text = page.read_text()
30
37
  for match in ATTRIBUTE.finditer(text):
@@ -39,6 +46,8 @@ def check_links(output: Path) -> list[str]:
39
46
  if not target.is_relative_to(root):
40
47
  problems.append(f"{where}: {reference} leaves the site")
41
48
  continue
49
+ if any(target == folder or target.is_relative_to(folder) for folder in retained):
50
+ continue
42
51
  if not target.exists():
43
52
  problems.append(f"{where}: missing {reference}")
44
53
  continue
@@ -132,7 +132,7 @@ from .scm import (
132
132
  Sensitivity,
133
133
  )
134
134
 
135
- __version__ = "0.2.0"
135
+ __version__ = "0.2.1"
136
136
 
137
137
  __all__ = [
138
138
  "GRAPH_FORMAT",
@@ -46,19 +46,40 @@ def _is_boundary(pair: tuple[str, str]) -> bool:
46
46
  return pair[0] == START or pair[1] == END
47
47
 
48
48
 
49
- @dataclass(frozen=True)
49
+ def _same_gap(a: float, b: float) -> bool:
50
+ return a == b or (math.isnan(a) and math.isnan(b))
51
+
52
+
53
+ @dataclass(frozen=True, eq=False)
50
54
  class ArcStats:
51
55
  """Traversal count and gap timing for one arc of the map.
52
56
 
53
57
  ``mean_gap`` and ``median_gap`` are seconds between the two events. A
54
- START or END arc has no second event, so both are NaN there (the one
55
- shared ``math.nan`` object, which keeps identically built maps equal).
58
+ START or END arc has no second event, so both are NaN there. Equality treats
59
+ two NaN gaps as equal, so identically built maps compare equal on every
60
+ Python (dataclass equality compares fields one by one from 3.13 on, where
61
+ ``nan == nan`` is false).
56
62
  """
57
63
 
58
64
  count: int
59
65
  mean_gap: float
60
66
  median_gap: float
61
67
 
68
+ def __eq__(self, other: object) -> bool:
69
+ if not isinstance(other, ArcStats):
70
+ return NotImplemented
71
+ return (
72
+ self.count == other.count
73
+ and _same_gap(self.mean_gap, other.mean_gap)
74
+ and _same_gap(self.median_gap, other.median_gap)
75
+ )
76
+
77
+ def __hash__(self) -> int:
78
+ def key(gap: float) -> float | None:
79
+ return None if math.isnan(gap) else gap
80
+
81
+ return hash((self.count, key(self.mean_gap), key(self.median_gap)))
82
+
62
83
 
63
84
  @dataclass(frozen=True)
64
85
  class ProcessMap:
@@ -51,11 +51,6 @@ def _features(X: Any) -> npt.NDArray[np.float64]:
51
51
  matrix = np.asarray(X, dtype=np.float64)
52
52
  if matrix.ndim != 2:
53
53
  raise ValueError(f"X must be a 2-D matrix; got shape {matrix.shape}")
54
- if matrix.shape[1] == 0:
55
- raise ValueError(
56
- "WeltLearner needs at least one feature column, and this slot has none (an "
57
- "empty adjustment set). Leave this slot to the default learner"
58
- )
59
54
  if matrix.shape[0] == 0:
60
55
  raise ValueError("X has no rows")
61
56
  return matrix
@@ -85,6 +80,11 @@ class WeltLearner:
85
80
  is a paid prediction, sent in batches no larger than the model's prediction-row
86
81
  limit. DML with ``n_folds=k`` fits ``2 * k`` times.
87
82
 
83
+ A slot with no feature columns (an empty adjustment set) has nothing for a model
84
+ to learn: its conditional mean is the training mean, or the class frequencies for
85
+ classification. The learner fits that locally, with no upload, no paid fit and no
86
+ charge to ``max_fits``, and ``predictor_id_`` is ``None``.
87
+
88
88
  The learner and its clones share one budget of ``max_fits`` fits. It caps paid
89
89
  fits only, not predictions: each fit is followed by one prediction call per
90
90
  batch of held-out rows (DML on 8,000 rows with five folds and a 100-row limit
@@ -113,8 +113,10 @@ class WeltLearner:
113
113
  leaves the job running in Welt.
114
114
 
115
115
  Attributes:
116
- predictor_id_: The fitted Welt predictor's id.
117
- model_version_: The exact model version Welt fitted.
116
+ predictor_id_: The fitted Welt predictor's id; ``None`` after a no-feature
117
+ (local) fit.
118
+ model_version_: The exact model version Welt fitted; ``None`` after a
119
+ no-feature fit.
118
120
  classes_: The class labels, sorted (classification only).
119
121
  supports_sample_weight: Always ``False``; read by ergodic's weighted slots.
120
122
  """
@@ -224,7 +226,8 @@ class WeltLearner:
224
226
  """Fit a Welt predictor on these rows: one paid fit.
225
227
 
226
228
  Args:
227
- X: The feature matrix, one row per unit and at least one column.
229
+ X: The feature matrix, one row per unit. With no columns the fit is the
230
+ local training mean (or class frequencies) and costs nothing.
228
231
  y: The target, one value per row: numbers for regression, labels for
229
232
  classification.
230
233
  sample_weight: Must be ``None``. Welt fits take no row weights, and a
@@ -234,8 +237,8 @@ class WeltLearner:
234
237
  This learner, fitted.
235
238
 
236
239
  Raises:
237
- ValueError: ``sample_weight`` was given, ``X`` has no columns or rows,
238
- or ``y`` does not match ``X``. Nothing is uploaded.
240
+ ValueError: ``sample_weight`` was given, ``X`` has no rows, or ``y``
241
+ does not match ``X``. Nothing is uploaded.
239
242
  RuntimeError: The shared ``max_fits`` budget is spent.
240
243
  """
241
244
  if sample_weight is not None:
@@ -254,10 +257,12 @@ class WeltLearner:
254
257
  raise ValueError("regression targets must be finite")
255
258
  else:
256
259
  labels = [_label(value) for value in target]
260
+ for name in ("predictor_id_", "model_version_", "classes_", "_order", "_local"):
261
+ self.__dict__.pop(name, None)
262
+ if matrix.shape[1] == 0:
263
+ return self._fit_local(labels)
257
264
  columns = [f"x{i}" for i in range(matrix.shape[1])]
258
265
  rows = [[*row, label] for row, label in zip(matrix_rows(matrix), labels, strict=True)]
259
- for name in ("predictor_id_", "model_version_", "classes_", "_order"):
260
- self.__dict__.pop(name, None)
261
266
  with using(self.client) as client:
262
267
  # The free catalogue read comes first, so a failed lookup costs no fit
263
268
  # and cannot strand a paid predictor.
@@ -278,8 +283,8 @@ class WeltLearner:
278
283
  predictor = job.result(timeout=self.timeout)
279
284
  if predictor.get("task") != self.task:
280
285
  raise ValueError("Welt fitted a predictor for a different task")
281
- self.predictor_id_: str = predictor["id"]
282
- self.model_version_: str = predictor["model_version"]
286
+ self.predictor_id_: str | None = predictor["id"]
287
+ self.model_version_: str | None = predictor["model_version"]
283
288
  self._n_features = matrix.shape[1]
284
289
  if self.task == "classification":
285
290
  classes = np.asarray(predictor["classes"])
@@ -287,6 +292,20 @@ class WeltLearner:
287
292
  self.classes_ = classes[self._order]
288
293
  return self
289
294
 
295
+ def _fit_local(self, labels: list[Any]) -> Self:
296
+ """The no-feature fit: the training mean, or the class frequencies, kept here."""
297
+ self.predictor_id_ = None
298
+ self.model_version_ = None
299
+ self._n_features = 0
300
+ if self.task == "regression":
301
+ self._local: npt.NDArray[Any] = np.asarray([np.mean(labels)], dtype=np.float64)
302
+ return self
303
+ classes, counts = np.unique(np.asarray(labels), return_counts=True)
304
+ self.classes_ = classes
305
+ self._order = np.arange(len(classes))
306
+ self._local = counts / counts.sum()
307
+ return self
308
+
290
309
  def _limit(self, client: Any) -> int | None:
291
310
  """The model's prediction-row limit from Welt's catalogue, if it states one."""
292
311
  for profile in client.model(self.model).get("task_profiles", []):
@@ -295,12 +314,16 @@ class WeltLearner:
295
314
  return int(limit) if limit else None
296
315
  return None
297
316
 
298
- def _predict(self, X: npt.NDArray[np.float64], probabilities: bool) -> list[dict[str, Any]]:
317
+ def _checked(self, X: npt.NDArray[np.float64]) -> npt.NDArray[np.float64]:
299
318
  if not hasattr(self, "predictor_id_"):
300
319
  raise RuntimeError("WeltLearner is not fitted; call fit first")
301
320
  matrix = _features(X)
302
321
  if matrix.shape[1] != self._n_features:
303
322
  raise ValueError(f"X has {matrix.shape[1]} columns; the fit had {self._n_features}")
323
+ return matrix
324
+
325
+ def _predict(self, X: npt.NDArray[np.float64], probabilities: bool) -> list[dict[str, Any]]:
326
+ matrix = self._checked(X)
304
327
  columns = [f"x{i}" for i in range(matrix.shape[1])]
305
328
  rows = matrix_rows(matrix)
306
329
  size = self._budget.prediction_rows.get((self.model, self.task)) or len(rows)
@@ -334,6 +357,11 @@ class WeltLearner:
334
357
  Numbers in the target's units for regression, class labels for
335
358
  classification.
336
359
  """
360
+ if "_local" in self.__dict__:
361
+ n = self._checked(X).shape[0]
362
+ if self.task == "regression":
363
+ return np.full(n, self._local[0], dtype=np.float64)
364
+ return np.full(n, self.classes_[int(np.argmax(self._local))])
337
365
  answers = self._predict(X, probabilities=False)
338
366
  values = [value for answer in answers for value in answer["predictions"]]
339
367
  if self.task == "regression":
@@ -341,6 +369,8 @@ class WeltLearner:
341
369
  return np.asarray(values)
342
370
 
343
371
  def _predict_proba(self, X: npt.NDArray[np.float64]) -> npt.NDArray[np.float64]:
372
+ if "_local" in self.__dict__:
373
+ return np.tile(self._local, (self._checked(X).shape[0], 1))
344
374
  answers = self._predict(X, probabilities=True)
345
375
  blocks = []
346
376
  for answer in answers:
@@ -26,9 +26,7 @@ from scripts.docsite.examples import programs_of # noqa: E402
26
26
  from tests.test_welt import FakeWelt # noqa: E402
27
27
 
28
28
 
29
- def test_the_welt_page_program_runs_against_the_fake(
30
- monkeypatch: pytest.MonkeyPatch, tmp_path: Path
31
- ) -> None:
29
+ def _run_page_program(fake: FakeWelt, monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> list[str]:
32
30
  site = load_site()
33
31
  page = site.pages_dir / "welt.md"
34
32
  (program,) = [p for p in programs_of(page, site) if p.name == "welt_effect.py"]
@@ -36,7 +34,6 @@ def test_the_welt_page_program_runs_against_the_fake(
36
34
 
37
35
  monkeypatch.delenv("WELT_WORKSPACE", raising=False)
38
36
  monkeypatch.delenv("WELT_BASE_URL", raising=False)
39
- fake = FakeWelt()
40
37
  real = welt.Client
41
38
 
42
39
  def client(*args: Any, **kwargs: Any) -> Any:
@@ -49,7 +46,14 @@ def test_the_welt_page_program_runs_against_the_fake(
49
46
  printed = io.StringIO()
50
47
  with contextlib.redirect_stdout(printed):
51
48
  runpy.run_path(str(script), run_name="__main__")
52
- lines = printed.getvalue().splitlines()
49
+ return printed.getvalue().splitlines()
50
+
51
+
52
+ def test_the_welt_page_program_runs_against_the_fake(
53
+ monkeypatch: pytest.MonkeyPatch, tmp_path: Path
54
+ ) -> None:
55
+ fake = FakeWelt()
56
+ lines = _run_page_program(fake, monkeypatch, tmp_path)
53
57
 
54
58
  # The fake's discovery returns a chain over the uploaded column order (the last
55
59
  # column left unconnected), so the page's story holds: a path from nprenatal to
@@ -69,6 +73,34 @@ def test_the_welt_page_program_runs_against_the_fake(
69
73
  assert sent[("POST", "datasets")] == 5
70
74
 
71
75
 
76
+ def test_the_welt_page_program_runs_when_the_graph_leaves_nothing_to_adjust(
77
+ monkeypatch: pytest.MonkeyPatch, tmp_path: Path
78
+ ) -> None:
79
+ """The graph production Arrow returned on 2026-10-15: nprenatal has no parents.
80
+
81
+ The adjustment set is empty, so both DML slots are training means fitted locally:
82
+ the page still prints an estimate, and the only paid request is the discovery.
83
+ """
84
+ edges = [
85
+ ("nprenatal", "bweight"),
86
+ ("fage", "mage"),
87
+ ("fage", "medu"),
88
+ ("mage", "medu"),
89
+ ]
90
+ flat = [[0.0 if i == j else 0.25 for j in range(5)] for i in range(5)]
91
+ fake = FakeWelt(native={"arrow": {"values": flat, "edges": edges, "decoder": {"name": "x"}}})
92
+ lines = _run_page_program(fake, monkeypatch, tmp_path)
93
+
94
+ assert "bweight <-- nprenatal" in lines
95
+ assert any(line.startswith("Identification(adjustment:") for line in lines)
96
+ assert any(line.startswith("EffectEstimate(dml, analytic:") for line in lines)
97
+ assert lines[-1] == "paid fits: 0"
98
+ sent = Counter((method, path.split("/")[2]) for method, path in fake.calls)
99
+ assert sent[("POST", "discoveries")] == 1
100
+ assert sent[("POST", "fits")] == 0
101
+ assert sent[("POST", "predictors")] == 0
102
+
103
+
72
104
  # CDFM in the fake: an adaptive threshold of 0.3 and four native edges above it, three
73
105
  # of which (medu -> nprenatal -> bweight -> medu) form a cycle.
74
106
  _COLUMNS = ["mage", "medu", "nprenatal", "bweight", "fage"]
@@ -227,7 +227,12 @@ def test_build_is_versioned_and_linked(tmp_path: Path) -> None:
227
227
  changelog = (output / "changelog.html").read_text()
228
228
  release = re.search(r"^## \[(\d+\.\d+\.\d+)\]", (ROOT / "CHANGELOG.md").read_text(), re.M)
229
229
  assert release is not None and f">{release[1]}</a> - " in changelog
230
- assert check_links(output) == []
230
+ # The retained versions come from their own tags' builds; until those are placed,
231
+ # links into them are the only problems, and --retained-elsewhere excuses them.
232
+ assert check_links(output, site.previous_versions) == []
233
+ missing = check_links(output)
234
+ assert missing # The picker links to every retained version.
235
+ assert all(any(f"v{v}/" in problem for v in site.previous_versions) for problem in missing)
231
236
 
232
237
 
233
238
  def test_build_fails_on_an_undocumented_public_name(
@@ -214,6 +214,17 @@ def test_performance_unknown_lifecycle_column_raises() -> None:
214
214
  # -- determinism ---------------------------------------------------------------
215
215
 
216
216
 
217
+ def test_arc_stats_with_nan_gaps_compare_and_hash_equal() -> None:
218
+ from ergodic.process._map import ArcStats
219
+
220
+ # Distinct NaN objects, so equality cannot lean on identity.
221
+ left = ArcStats(count=2, mean_gap=float("nan"), median_gap=float("nan"))
222
+ right = ArcStats(count=2, mean_gap=float("nan"), median_gap=float("nan"))
223
+ assert left == right and hash(left) == hash(right)
224
+ assert left != ArcStats(count=3, mean_gap=float("nan"), median_gap=float("nan"))
225
+ assert ArcStats(1, 2.0, 2.0) != ArcStats(1, 2.0, float("nan"))
226
+
227
+
217
228
  def test_repeated_calls_give_equal_objects() -> None:
218
229
  plain = _two_case_log()
219
230
  skewed = _skewed_log()
@@ -588,9 +588,45 @@ def test_sample_weight_is_refused_not_ignored() -> None:
588
588
  assert learner.fits_used == 0
589
589
 
590
590
 
591
- def test_an_empty_feature_matrix_is_refused_before_a_fit() -> None:
592
- with pytest.raises(ValueError, match="at least one feature"):
593
- WeltLearner(client=object()).fit(np.empty((5, 0)), np.arange(5.0))
591
+ def test_a_slot_with_no_features_is_fitted_locally_for_free() -> None:
592
+ """An empty adjustment set leaves the mean (or class frequencies): no request, no fit."""
593
+ fake = FakeWelt()
594
+ with fake.client() as client:
595
+ regression = WeltLearner(client=client, max_fits=1).fit(
596
+ np.empty((4, 0)), np.array([1.0, 2, 3, 6])
597
+ )
598
+ np.testing.assert_allclose(regression.predict(np.empty((3, 0))), [3.0, 3.0, 3.0])
599
+ classifier = WeltLearner(task="classification", client=client, max_fits=1)
600
+ classifier.fit(np.empty((4, 0)), np.array([1.0, 0.0, 1.0, 1.0]))
601
+ np.testing.assert_allclose(classifier.predict_proba(np.empty((2, 0))), [[0.25, 0.75]] * 2)
602
+ assert classifier.predict(np.empty((2, 0))).tolist() == [1.0, 1.0]
603
+ with pytest.raises(ValueError, match="columns"):
604
+ regression.predict(np.ones((3, 1)))
605
+ assert fake.calls == []
606
+ assert regression.fits_used == 0 and classifier.fits_used == 0
607
+ assert regression.predictor_id_ is None and regression.model_version_ is None
608
+ assert classifier.classes_.tolist() == [0, 1]
609
+ with pytest.raises(ValueError, match="no rows"):
610
+ WeltLearner(client=object()).fit(np.empty((0, 0)), np.array([]))
611
+
612
+
613
+ def test_dml_with_an_empty_adjustment_set_pays_for_nothing() -> None:
614
+ """No confounders: both DML nuisances are training means, exactly as ergodic's OLS."""
615
+ fake = FakeWelt()
616
+ data = continuous(n=200).select(["X", "Y"])
617
+ graph = dag(["X -> Y"])
618
+ native = estimate_effect(
619
+ graph, data, "X", "Y", strategy="dml", learner="ols", random_state=3, n_folds=2
620
+ )
621
+ with fake.client() as client:
622
+ learner = WeltLearner("tabicl-v2", client=client)
623
+ hosted = estimate_effect(
624
+ graph, data, "X", "Y", strategy="dml", learner=learner, random_state=3, n_folds=2
625
+ )
626
+ assert hosted.value == pytest.approx(native.value, rel=1e-9)
627
+ assert hosted.std_error == pytest.approx(native.std_error, rel=1e-9)
628
+ assert learner.fits_used == 0
629
+ assert fake.calls == []
594
630
 
595
631
 
596
632
  def test_regression_fit_and_batched_predict() -> None:
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes