do-attribution 0.0.2__tar.gz → 0.0.3__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 (208) hide show
  1. {do_attribution-0.0.2/do_attribution.egg-info → do_attribution-0.0.3}/PKG-INFO +317 -77
  2. {do_attribution-0.0.2 → do_attribution-0.0.3}/README.md +313 -72
  3. {do_attribution-0.0.2 → do_attribution-0.0.3}/do_attribution/__init__.py +5 -0
  4. do_attribution-0.0.3/do_attribution/_static_export.py +128 -0
  5. {do_attribution-0.0.2 → do_attribution-0.0.3}/do_attribution/_temporal_helpers.py +76 -2
  6. do_attribution-0.0.3/do_attribution/baselines.py +192 -0
  7. {do_attribution-0.0.2 → do_attribution-0.0.3}/do_attribution/calibration.py +290 -46
  8. {do_attribution-0.0.2 → do_attribution-0.0.3}/do_attribution/compare.py +134 -4
  9. {do_attribution-0.0.2 → do_attribution-0.0.3}/do_attribution/markov.py +114 -33
  10. {do_attribution-0.0.2 → do_attribution-0.0.3}/do_attribution/reporting.py +77 -7
  11. do_attribution-0.0.3/do_attribution/shapley.py +668 -0
  12. {do_attribution-0.0.2 → do_attribution-0.0.3}/do_attribution/temporal_markov.py +54 -67
  13. {do_attribution-0.0.2 → do_attribution-0.0.3}/do_attribution/temporal_shapley.py +91 -81
  14. do_attribution-0.0.3/do_attribution/utils.py +571 -0
  15. {do_attribution-0.0.2 → do_attribution-0.0.3/do_attribution.egg-info}/PKG-INFO +317 -77
  16. {do_attribution-0.0.2 → do_attribution-0.0.3}/do_attribution.egg-info/SOURCES.txt +10 -2
  17. {do_attribution-0.0.2 → do_attribution-0.0.3}/do_attribution.egg-info/requires.txt +2 -2
  18. {do_attribution-0.0.2 → do_attribution-0.0.3}/docs/calibration_diagnostics.md +55 -5
  19. do_attribution-0.0.3/examples/attribution_experiment_calibration.ipynb +3327 -0
  20. do_attribution-0.0.3/examples/basic_markov.ipynb +716 -0
  21. do_attribution-0.0.3/examples/basic_shapley.ipynb +807 -0
  22. {do_attribution-0.0.2 → do_attribution-0.0.3}/examples/calibration_workflow.py +330 -0
  23. {do_attribution-0.0.2 → do_attribution-0.0.3}/examples/generate_calibration_visuals.py +17 -21
  24. do_attribution-0.0.3/examples/images/calibration_experimental_evidence.png +0 -0
  25. do_attribution-0.0.3/examples/images/calibration_static_effects.png +0 -0
  26. do_attribution-0.0.3/examples/images/calibration_temporal_effects.png +0 -0
  27. do_attribution-0.0.3/examples/images/calibration_temporal_entity_mix.png +0 -0
  28. do_attribution-0.0.3/examples/images/calibration_temporal_evidence_uncertainty.png +0 -0
  29. do_attribution-0.0.3/examples/images/calibration_temporal_residual_allocation.png +0 -0
  30. do_attribution-0.0.3/examples/images/ga4_model_comparison.png +0 -0
  31. do_attribution-0.0.3/examples/images/ga4_shapley_attribution.png +0 -0
  32. do_attribution-0.0.3/examples/images/ga4_temporal_markov.png +0 -0
  33. do_attribution-0.0.3/examples/images/ga4_temporal_shapley.png +0 -0
  34. {do_attribution-0.0.2 → do_attribution-0.0.3}/examples/methodological_stress_tests.ipynb +116 -63
  35. do_attribution-0.0.3/examples/side_by_side.ipynb +533 -0
  36. do_attribution-0.0.3/examples/temporal_markov.ipynb +1486 -0
  37. do_attribution-0.0.3/examples/temporal_shapley.ipynb +1889 -0
  38. do_attribution-0.0.3/examples/weighted_attribution.ipynb +1416 -0
  39. {do_attribution-0.0.2 → do_attribution-0.0.3}/pyproject.toml +4 -6
  40. do_attribution-0.0.3/tests/test_baselines.py +346 -0
  41. {do_attribution-0.0.2 → do_attribution-0.0.3}/tests/test_calibration_cross_cutting_guarantees.py +1 -0
  42. {do_attribution-0.0.2 → do_attribution-0.0.3}/tests/test_calibration_docs_diagnostics.py +5 -4
  43. {do_attribution-0.0.2 → do_attribution-0.0.3}/tests/test_calibration_repaired_contracts.py +3 -1
  44. {do_attribution-0.0.2 → do_attribution-0.0.3}/tests/test_calibration_reporting_gate.py +1 -0
  45. {do_attribution-0.0.2 → do_attribution-0.0.3}/tests/test_calibration_shared_gate.py +1 -0
  46. do_attribution-0.0.3/tests/test_calibration_standard_error_contract.py +232 -0
  47. {do_attribution-0.0.2 → do_attribution-0.0.3}/tests/test_calibration_summary.py +64 -0
  48. {do_attribution-0.0.2 → do_attribution-0.0.3}/tests/test_calibration_visual_artifacts.py +25 -3
  49. do_attribution-0.0.3/tests/test_calibration_workflow_example.py +631 -0
  50. do_attribution-0.0.3/tests/test_compare.py +754 -0
  51. {do_attribution-0.0.2 → do_attribution-0.0.3}/tests/test_installed_package.py +7 -1
  52. {do_attribution-0.0.2 → do_attribution-0.0.3}/tests/test_markov.py +142 -4
  53. do_attribution-0.0.3/tests/test_notebook_contracts.py +286 -0
  54. do_attribution-0.0.3/tests/test_readme_examples.py +182 -0
  55. do_attribution-0.0.3/tests/test_shapley.py +1138 -0
  56. do_attribution-0.0.3/tests/test_temporal_annual_frequencies.py +100 -0
  57. {do_attribution-0.0.2 → do_attribution-0.0.3}/tests/test_temporal_calibration_summary.py +51 -0
  58. do_attribution-0.0.3/tests/test_temporal_cross_year_epochs.py +267 -0
  59. {do_attribution-0.0.2 → do_attribution-0.0.3}/tests/test_temporal_entity_mix_plot.py +28 -0
  60. {do_attribution-0.0.2 → do_attribution-0.0.3}/tests/test_temporal_epoch_alignment_plot.py +50 -0
  61. {do_attribution-0.0.2 → do_attribution-0.0.3}/tests/test_temporal_evidence_export.py +84 -0
  62. {do_attribution-0.0.2 → do_attribution-0.0.3}/tests/test_temporal_evidence_strength_plot.py +143 -15
  63. do_attribution-0.0.3/tests/test_temporal_evidence_uncertainty_plot.py +298 -0
  64. {do_attribution-0.0.2 → do_attribution-0.0.3}/tests/test_temporal_markov.py +53 -0
  65. {do_attribution-0.0.2 → do_attribution-0.0.3}/tests/test_temporal_ratio_schedule_plot.py +153 -0
  66. {do_attribution-0.0.2 → do_attribution-0.0.3}/tests/test_temporal_shapley.py +172 -5
  67. {do_attribution-0.0.2 → do_attribution-0.0.3}/tests/test_temporal_v2_mixed_freq.py +3 -1
  68. do_attribution-0.0.3/tests/test_to_dataframes.py +1125 -0
  69. do_attribution-0.0.3/tests/test_utils.py +613 -0
  70. {do_attribution-0.0.2 → do_attribution-0.0.3}/tests/test_visualization.py +27 -0
  71. do_attribution-0.0.2/do_attribution/shapley.py +0 -304
  72. do_attribution-0.0.2/do_attribution/utils.py +0 -299
  73. do_attribution-0.0.2/examples/attribution_experiment_calibration.ipynb +0 -3219
  74. do_attribution-0.0.2/examples/basic_markov.ipynb +0 -749
  75. do_attribution-0.0.2/examples/basic_shapley.ipynb +0 -784
  76. do_attribution-0.0.2/examples/images/calibration_static_effects.png +0 -0
  77. do_attribution-0.0.2/examples/images/calibration_temporal_effects.png +0 -0
  78. do_attribution-0.0.2/examples/images/calibration_temporal_entity_mix.png +0 -0
  79. do_attribution-0.0.2/examples/images/calibration_temporal_evidence_strength.png +0 -0
  80. do_attribution-0.0.2/examples/images/calibration_temporal_ratio_schedule.png +0 -0
  81. do_attribution-0.0.2/examples/images/calibration_temporal_residual_allocation.png +0 -0
  82. do_attribution-0.0.2/examples/images/ga4_model_comparison.png +0 -0
  83. do_attribution-0.0.2/examples/images/ga4_shapley_attribution.png +0 -0
  84. do_attribution-0.0.2/examples/images/ga4_temporal_markov.png +0 -0
  85. do_attribution-0.0.2/examples/images/ga4_temporal_shapley.png +0 -0
  86. do_attribution-0.0.2/examples/side_by_side.ipynb +0 -626
  87. do_attribution-0.0.2/examples/temporal_markov.ipynb +0 -1540
  88. do_attribution-0.0.2/examples/temporal_shapley.ipynb +0 -1551
  89. do_attribution-0.0.2/tests/test_calibration_standard_error_contract.py +0 -56
  90. do_attribution-0.0.2/tests/test_calibration_workflow_example.py +0 -258
  91. do_attribution-0.0.2/tests/test_compare.py +0 -141
  92. do_attribution-0.0.2/tests/test_notebook_contracts.py +0 -104
  93. do_attribution-0.0.2/tests/test_shapley.py +0 -403
  94. do_attribution-0.0.2/tests/test_to_dataframes.py +0 -434
  95. do_attribution-0.0.2/tests/test_utils.py +0 -236
  96. {do_attribution-0.0.2 → do_attribution-0.0.3}/LICENSE +0 -0
  97. {do_attribution-0.0.2 → do_attribution-0.0.3}/MANIFEST.in +0 -0
  98. {do_attribution-0.0.2 → do_attribution-0.0.3}/do_attribution/_calibration_validation.py +0 -0
  99. {do_attribution-0.0.2 → do_attribution-0.0.3}/do_attribution/base.py +0 -0
  100. {do_attribution-0.0.2 → do_attribution-0.0.3}/do_attribution/normalization.py +0 -0
  101. {do_attribution-0.0.2 → do_attribution-0.0.3}/do_attribution/temporal.py +0 -0
  102. {do_attribution-0.0.2 → do_attribution-0.0.3}/do_attribution/temporal_diagnostics.py +0 -0
  103. {do_attribution-0.0.2 → do_attribution-0.0.3}/do_attribution/visualization.py +0 -0
  104. {do_attribution-0.0.2 → do_attribution-0.0.3}/do_attribution.egg-info/dependency_links.txt +0 -0
  105. {do_attribution-0.0.2 → do_attribution-0.0.3}/do_attribution.egg-info/top_level.txt +0 -0
  106. {do_attribution-0.0.2 → do_attribution-0.0.3}/docs/calibration.md +0 -0
  107. {do_attribution-0.0.2 → do_attribution-0.0.3}/docs/calibration_examples.md +0 -0
  108. {do_attribution-0.0.2 → do_attribution-0.0.3}/docs/calibration_examples_conflicts.md +0 -0
  109. {do_attribution-0.0.2 → do_attribution-0.0.3}/docs/calibration_examples_schedules.md +0 -0
  110. {do_attribution-0.0.2 → do_attribution-0.0.3}/docs/calibration_examples_temporal.md +0 -0
  111. {do_attribution-0.0.2 → do_attribution-0.0.3}/examples/ga4_attribution_journeys.csv +0 -0
  112. {do_attribution-0.0.2 → do_attribution-0.0.3}/examples/images/calibration_temporal_epoch_alignment.png +0 -0
  113. {do_attribution-0.0.2 → do_attribution-0.0.3}/examples/images/ga4_coalition_values.png +0 -0
  114. {do_attribution-0.0.2 → do_attribution-0.0.3}/examples/images/ga4_markov_attribution.png +0 -0
  115. {do_attribution-0.0.2 → do_attribution-0.0.3}/examples/images/ga4_markov_epoch_recommendation.png +0 -0
  116. {do_attribution-0.0.2 → do_attribution-0.0.3}/examples/images/ga4_markov_removal_effects.png +0 -0
  117. {do_attribution-0.0.2 → do_attribution-0.0.3}/examples/images/ga4_shapley_epoch_recommendation.png +0 -0
  118. {do_attribution-0.0.2 → do_attribution-0.0.3}/examples/images/ga4_transition_graph.png +0 -0
  119. {do_attribution-0.0.2 → do_attribution-0.0.3}/requirements.txt +0 -0
  120. {do_attribution-0.0.2 → do_attribution-0.0.3}/setup.cfg +0 -0
  121. {do_attribution-0.0.2 → do_attribution-0.0.3}/setup.py +0 -0
  122. {do_attribution-0.0.2 → do_attribution-0.0.3}/tests/__init__.py +0 -0
  123. {do_attribution-0.0.2 → do_attribution-0.0.3}/tests/conftest.py +0 -0
  124. {do_attribution-0.0.2 → do_attribution-0.0.3}/tests/fixtures/calibration_api_contract.py +0 -0
  125. {do_attribution-0.0.2 → do_attribution-0.0.3}/tests/test_calibration_api_contract.py +0 -0
  126. {do_attribution-0.0.2 → do_attribution-0.0.3}/tests/test_calibration_contract.py +0 -0
  127. {do_attribution-0.0.2 → do_attribution-0.0.3}/tests/test_calibration_dataframe_export.py +0 -0
  128. {do_attribution-0.0.2 → do_attribution-0.0.3}/tests/test_calibration_docs_conflict_examples.py +0 -0
  129. {do_attribution-0.0.2 → do_attribution-0.0.3}/tests/test_calibration_docs_schedule_examples.py +0 -0
  130. {do_attribution-0.0.2 → do_attribution-0.0.3}/tests/test_calibration_docs_static_examples.py +0 -0
  131. {do_attribution-0.0.2 → do_attribution-0.0.3}/tests/test_calibration_docs_temporal_examples.py +0 -0
  132. {do_attribution-0.0.2 → do_attribution-0.0.3}/tests/test_calibration_evidence_collection.py +0 -0
  133. {do_attribution-0.0.2 → do_attribution-0.0.3}/tests/test_calibration_normalization.py +0 -0
  134. {do_attribution-0.0.2 → do_attribution-0.0.3}/tests/test_calibration_numeric_validation.py +0 -0
  135. {do_attribution-0.0.2 → do_attribution-0.0.3}/tests/test_calibration_package_exports.py +0 -0
  136. {do_attribution-0.0.2 → do_attribution-0.0.3}/tests/test_calibration_policy_contract.py +0 -0
  137. {do_attribution-0.0.2 → do_attribution-0.0.3}/tests/test_calibration_results.py +0 -0
  138. {do_attribution-0.0.2 → do_attribution-0.0.3}/tests/test_calibration_row.py +0 -0
  139. {do_attribution-0.0.2 → do_attribution-0.0.3}/tests/test_calibration_skeletons.py +0 -0
  140. {do_attribution-0.0.2 → do_attribution-0.0.3}/tests/test_calibration_validation_boundary.py +0 -0
  141. {do_attribution-0.0.2 → do_attribution-0.0.3}/tests/test_epoch_ratio_row.py +0 -0
  142. {do_attribution-0.0.2 → do_attribution-0.0.3}/tests/test_evidence_point_record.py +0 -0
  143. {do_attribution-0.0.2 → do_attribution-0.0.3}/tests/test_lift_test_evidence.py +0 -0
  144. {do_attribution-0.0.2 → do_attribution-0.0.3}/tests/test_normalization.py +0 -0
  145. {do_attribution-0.0.2 → do_attribution-0.0.3}/tests/test_package_metadata.py +0 -0
  146. {do_attribution-0.0.2 → do_attribution-0.0.3}/tests/test_ratio_schedule_row.py +0 -0
  147. {do_attribution-0.0.2 → do_attribution-0.0.3}/tests/test_static_allocation_feasibility.py +0 -0
  148. {do_attribution-0.0.2 → do_attribution-0.0.3}/tests/test_static_allow_total_change.py +0 -0
  149. {do_attribution-0.0.2 → do_attribution-0.0.3}/tests/test_static_calibration_effects_plot.py +0 -0
  150. {do_attribution-0.0.2 → do_attribution-0.0.3}/tests/test_static_calibration_properties.py +0 -0
  151. {do_attribution-0.0.2 → do_attribution-0.0.3}/tests/test_static_custom_weights.py +0 -0
  152. {do_attribution-0.0.2 → do_attribution-0.0.3}/tests/test_static_direct_adjustment.py +0 -0
  153. {do_attribution-0.0.2 → do_attribution-0.0.3}/tests/test_static_explicit_adjustments.py +0 -0
  154. {do_attribution-0.0.2 → do_attribution-0.0.3}/tests/test_static_generated_properties.py +0 -0
  155. {do_attribution-0.0.2 → do_attribution-0.0.3}/tests/test_static_markov_calibration_integration.py +0 -0
  156. {do_attribution-0.0.2 → do_attribution-0.0.3}/tests/test_static_multiple_tested_entities.py +0 -0
  157. {do_attribution-0.0.2 → do_attribution-0.0.3}/tests/test_static_proportional_untested.py +0 -0
  158. {do_attribution-0.0.2 → do_attribution-0.0.3}/tests/test_static_residual_bucket.py +0 -0
  159. {do_attribution-0.0.2 → do_attribution-0.0.3}/tests/test_static_residual_dispatch.py +0 -0
  160. {do_attribution-0.0.2 → do_attribution-0.0.3}/tests/test_static_residual_policy_matrix.py +0 -0
  161. {do_attribution-0.0.2 → do_attribution-0.0.3}/tests/test_static_shapley_calibration_integration.py +0 -0
  162. {do_attribution-0.0.2 → do_attribution-0.0.3}/tests/test_static_share_equivalence.py +0 -0
  163. {do_attribution-0.0.2 → do_attribution-0.0.3}/tests/test_temporal_allocation_provenance_export.py +0 -0
  164. {do_attribution-0.0.2 → do_attribution-0.0.3}/tests/test_temporal_allow_total_change.py +0 -0
  165. {do_attribution-0.0.2 → do_attribution-0.0.3}/tests/test_temporal_apply_estimates.py +0 -0
  166. {do_attribution-0.0.2 → do_attribution-0.0.3}/tests/test_temporal_calibration_construction.py +0 -0
  167. {do_attribution-0.0.2 → do_attribution-0.0.3}/tests/test_temporal_calibration_effects_plot.py +0 -0
  168. {do_attribution-0.0.2 → do_attribution-0.0.3}/tests/test_temporal_constant_schedule.py +0 -0
  169. {do_attribution-0.0.2 → do_attribution-0.0.3}/tests/test_temporal_coordinate_average.py +0 -0
  170. {do_attribution-0.0.2 → do_attribution-0.0.3}/tests/test_temporal_coordinate_linear_schedule.py +0 -0
  171. {do_attribution-0.0.2 → do_attribution-0.0.3}/tests/test_temporal_coordinate_normalization.py +0 -0
  172. {do_attribution-0.0.2 → do_attribution-0.0.3}/tests/test_temporal_coordinate_schedule_export.py +0 -0
  173. {do_attribution-0.0.2 → do_attribution-0.0.3}/tests/test_temporal_custom_weights.py +0 -0
  174. {do_attribution-0.0.2 → do_attribution-0.0.3}/tests/test_temporal_diagnostics.py +0 -0
  175. {do_attribution-0.0.2 → do_attribution-0.0.3}/tests/test_temporal_direct_epoch_ratio.py +0 -0
  176. {do_attribution-0.0.2 → do_attribution-0.0.3}/tests/test_temporal_epoch_coordinate_weights.py +0 -0
  177. {do_attribution-0.0.2 → do_attribution-0.0.3}/tests/test_temporal_epoch_coordinates.py +0 -0
  178. {do_attribution-0.0.2 → do_attribution-0.0.3}/tests/test_temporal_epoch_metadata_gate.py +0 -0
  179. {do_attribution-0.0.2 → do_attribution-0.0.3}/tests/test_temporal_epoch_ratio_dispatch.py +0 -0
  180. {do_attribution-0.0.2 → do_attribution-0.0.3}/tests/test_temporal_epoch_ratio_export.py +0 -0
  181. {do_attribution-0.0.2 → do_attribution-0.0.3}/tests/test_temporal_epoch_ratio_gate.py +0 -0
  182. {do_attribution-0.0.2 → do_attribution-0.0.3}/tests/test_temporal_epoch_support.py +0 -0
  183. {do_attribution-0.0.2 → do_attribution-0.0.3}/tests/test_temporal_epoch_windows.py +0 -0
  184. {do_attribution-0.0.2 → do_attribution-0.0.3}/tests/test_temporal_evidence_conflict_dispatch.py +0 -0
  185. {do_attribution-0.0.2 → do_attribution-0.0.3}/tests/test_temporal_evidence_conflict_gate.py +0 -0
  186. {do_attribution-0.0.2 → do_attribution-0.0.3}/tests/test_temporal_explicit_adjustments.py +0 -0
  187. {do_attribution-0.0.2 → do_attribution-0.0.3}/tests/test_temporal_explicit_ratio_schedule.py +0 -0
  188. {do_attribution-0.0.2 → do_attribution-0.0.3}/tests/test_temporal_explicit_strength_schedule.py +0 -0
  189. {do_attribution-0.0.2 → do_attribution-0.0.3}/tests/test_temporal_generated_properties.py +0 -0
  190. {do_attribution-0.0.2 → do_attribution-0.0.3}/tests/test_temporal_helper_equivalence.py +0 -0
  191. {do_attribution-0.0.2 → do_attribution-0.0.3}/tests/test_temporal_markov_calibration_integration.py +0 -0
  192. {do_attribution-0.0.2 → do_attribution-0.0.3}/tests/test_temporal_observed_only_schedule.py +0 -0
  193. {do_attribution-0.0.2 → do_attribution-0.0.3}/tests/test_temporal_pooled_lift.py +0 -0
  194. {do_attribution-0.0.2 → do_attribution-0.0.3}/tests/test_temporal_proportional_untested.py +0 -0
  195. {do_attribution-0.0.2 → do_attribution-0.0.3}/tests/test_temporal_ratio_policy_dispatch.py +0 -0
  196. {do_attribution-0.0.2 → do_attribution-0.0.3}/tests/test_temporal_ratio_schedule_gate.py +0 -0
  197. {do_attribution-0.0.2 → do_attribution-0.0.3}/tests/test_temporal_recommendation.py +0 -0
  198. {do_attribution-0.0.2 → do_attribution-0.0.3}/tests/test_temporal_reporting.py +0 -0
  199. {do_attribution-0.0.2 → do_attribution-0.0.3}/tests/test_temporal_representative_coordinate.py +0 -0
  200. {do_attribution-0.0.2 → do_attribution-0.0.3}/tests/test_temporal_residual_allocation_gate.py +0 -0
  201. {do_attribution-0.0.2 → do_attribution-0.0.3}/tests/test_temporal_residual_allocation_plot.py +0 -0
  202. {do_attribution-0.0.2 → do_attribution-0.0.3}/tests/test_temporal_residual_bucket.py +0 -0
  203. {do_attribution-0.0.2 → do_attribution-0.0.3}/tests/test_temporal_residual_policy_dispatch.py +0 -0
  204. {do_attribution-0.0.2 → do_attribution-0.0.3}/tests/test_temporal_select_evidence.py +0 -0
  205. {do_attribution-0.0.2 → do_attribution-0.0.3}/tests/test_temporal_shapley_calibration_integration.py +0 -0
  206. {do_attribution-0.0.2 → do_attribution-0.0.3}/tests/test_temporal_share_equivalence.py +0 -0
  207. {do_attribution-0.0.2 → do_attribution-0.0.3}/tests/test_temporal_support_weighted_average.py +0 -0
  208. {do_attribution-0.0.2 → do_attribution-0.0.3}/tests/test_temporal_weighted_ratio.py +0 -0
@@ -1,22 +1,21 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: do-attribution
3
- Version: 0.0.2
3
+ Version: 0.0.3
4
4
  Summary: Unified multi-touch attribution library (Markov chain + Shapley value).
5
5
  Author-email: Aaron Goodin <goodinaaron@causalscience.com>
6
6
  License-Expression: MIT
7
7
  Keywords: attribution,marketing,markov,shapley,multi-touch,analytics,incrementality
8
8
  Classifier: Programming Language :: Python :: 3
9
- Classifier: Programming Language :: Python :: 3.10
10
9
  Classifier: Programming Language :: Python :: 3.11
11
10
  Classifier: Programming Language :: Python :: 3.12
12
11
  Classifier: Operating System :: OS Independent
13
12
  Classifier: Intended Audience :: Science/Research
14
13
  Classifier: Topic :: Scientific/Engineering :: Information Analysis
15
- Requires-Python: >=3.10
14
+ Requires-Python: >=3.11
16
15
  Description-Content-Type: text/markdown
17
16
  License-File: LICENSE
18
- Requires-Dist: numpy>=1.21.0
19
- Requires-Dist: pandas>=1.3.0
17
+ Requires-Dist: numpy>=2.0.2
18
+ Requires-Dist: pandas>=3.1.1
20
19
  Provides-Extra: viz
21
20
  Requires-Dist: matplotlib>=3.3.0; extra == "viz"
22
21
  Requires-Dist: networkx>=2.5; extra == "viz"
@@ -41,7 +40,7 @@ closed-form Markov, exact Shapley, temporal attribution, and explicit
41
40
  calibration from external lift-test evidence.**
42
41
 
43
42
  ![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)
44
- ![Python: 3.10+](https://img.shields.io/badge/python-3.10%2B-blue.svg)
43
+ ![Python: 3.11+](https://img.shields.io/badge/python-3.11%2B-blue.svg)
45
44
 
46
45
  `do-attribution` has two explicit layers:
47
46
 
@@ -59,7 +58,7 @@ allocation models:
59
58
  - an **absorbing-Markov-chain removal-effect** model
60
59
  (`MarkovAttribution`, orders 1+, four removal strategies), and
61
60
  - a **cooperative-game Shapley-value** model (`ShapleyAttribution`, exact
62
- factorial weights over the observed coalition lattice).
61
+ Shapley values of a coalition game built from the observed channel sets).
63
62
 
64
63
  The calibration layer applies caller-supplied lift-test evidence after a
65
64
  static or temporal attribution fit. Direct adjustments to tested entities,
@@ -94,11 +93,22 @@ chronological epochs.
94
93
  `"fail_refit"` (signed effects), `"detour"` (signed effects), or
95
94
  `"redirect_to_null"` — four operational meanings of "remove this channel",
96
95
  selected explicitly instead of implied.
97
- - **Exact Shapley values.** Per-channel values use the exact factorial weight
98
- `w(s, n) = s!·(n−s−1)!/n!`. Raw signed values and the coalition value
99
- function are always returned; normalized shares are returned when the raw
100
- values admit a valid share vector, and reported as unavailable when they
101
- do not.
96
+ - **Exact Shapley values.** The default cumulative coalition game credits a
97
+ coalition with every conversion on journeys whose channels it contains, so
98
+ the Shapley values are a non-negative allocation of all observed
99
+ conversions. They are exact — no sampling — and equal the factorial-weight
100
+ definition `w(s, n) = s!·(n−s−1)!/n!`. Raw values and the observed
101
+ coalition table are always returned; normalized shares are returned when
102
+ the raw values admit a valid share vector, and reported as unavailable
103
+ when they do not. The legacy exact-set game remains available through
104
+ `characteristic_function="exact"`.
105
+ - **Optional observation weights.** `ShapleyAttribution.fit` and
106
+ `CompareAttribution.fit` accept per-journey `weights=` — frequency counts,
107
+ sampling weights, or any other nonnegative weight — under the same rules
108
+ as `MarkovAttribution.fit`. Weighted coalition totals are kept as
109
+ fractions, zero-weight journeys are excluded, and a weighted comparison
110
+ fits Markov and Shapley on the same weighted journeys. A fit without
111
+ weights is unchanged.
102
112
  - **Conservation and contract guarantees.** When normalized attribution is
103
113
  available its shares are finite, lie on `[0, 1]`, and sum to 1, and
104
114
  attributed conversions conserve the observed total. When it is not,
@@ -110,10 +120,10 @@ chronological epochs.
110
120
  or a natural-language `summary(output_format="report")` — plain Python
111
121
  strings with bounded, distilled detail tables (signed removal effects, top
112
122
  transitions and drop-offs, top coalitions, path diagnostics).
113
- - **Flat DataFrame export.** Temporal models expose
123
+ - **Flat DataFrame export.** Static and temporal models expose
114
124
  `to_dataframes() -> dict[str, pd.DataFrame]`: flat, string-keyed,
115
- Parquet/SQL-ready tables for attribution, raw model values, and sample
116
- diagnostics — no text parsing, no `MultiIndex`, no lazy refits.
125
+ Parquet/SQL-ready tables for attribution, raw model values, and fit or
126
+ sample diagnostics — no text parsing, no `MultiIndex`, no lazy refits.
117
127
  - **Visualization.** `plot_attribution`, `plot_removal_effects`,
118
128
  `plot_transition_graph`, `plot_coalition_values`,
119
129
  `plot_attribution_comparison`, and `plot_epoch_recommendation` each return
@@ -127,8 +137,9 @@ chronological epochs.
127
137
  adjustments, residual allocation, evidence schedules, epoch-level
128
138
  reductions, and provenance explicitly separate.
129
139
  - **Calibration diagnostics.** One static adjustment-decomposition plot and
130
- six temporal diagnostics show evidence alignment, ratio construction,
131
- direct adjustments, residual allocation, entity mix, and evidence strength.
140
+ seven temporal diagnostics show evidence alignment, ratio construction,
141
+ experiment-reported evidence uncertainty, direct adjustments, residual
142
+ allocation, entity mix, and calibration-policy comparisons.
132
143
  - **Reproducible outputs.** Deterministic model fits, deterministic
133
144
  renderers, and deterministic tie ordering — identical inputs always
134
145
  produce identical reports, tables, and figures.
@@ -155,7 +166,7 @@ pip install "do-attribution[viz]"
155
166
  pip install "do-attribution[temporal,viz]"
156
167
  ```
157
168
 
158
- Requires Python **3.10 or newer**.
169
+ Requires Python 3.11 or newer, NumPy 2.0.2 or newer, and pandas 3.1.1 or newer.
159
170
 
160
171
  ---
161
172
 
@@ -223,30 +234,47 @@ Removal effects (raw, signed) vs normalized shares
223
234
  +---------+----------------+--------+
224
235
  ```
225
236
 
226
- The Shapley model is a drop-in alternative on the same inputs. Note that
227
- `attribution` may be `None` — always check before indexing it:
237
+ The Shapley model is a drop-in alternative on the same inputs:
228
238
 
229
239
  ```python
230
240
  from do_attribution import ShapleyAttribution
231
241
 
232
242
  shapley = ShapleyAttribution(conversion_labels=["Purchase"]).fit(journeys, conversions)
233
243
 
234
- if shapley["attribution"] is None:
235
- print(shapley["normalization_status"], shapley["normalization_reason"])
236
- # unavailable near_cancellation
237
- print({ch: round(v, 4) for ch, v in shapley["shapley_values"].items()})
238
- # {'Direct': -0.5833, 'Email': 0.25, 'Search': -0.0833, 'Social': 0.4167}
239
- else:
240
- print({ch: round(share, 4) for ch, share in shapley["attribution"].items()})
244
+ print(shapley["normalization_status"])
245
+ # available
246
+ print({ch: round(share, 4) for ch, share in shapley["attribution"].items()})
247
+ # {'Direct': 0.1429, 'Email': 0.3095, 'Search': 0.1667, 'Social': 0.381}
248
+ print({ch: round(v, 4) for ch, v in shapley["shapley_values"].items()})
249
+ # {'Direct': 1.0, 'Email': 2.1667, 'Search': 1.1667, 'Social': 2.6667}
241
250
  ```
242
251
 
243
- On this dataset no journey touches all four channels, so `v(N) = 0`, the raw
244
- values cancel to float residue, and no valid share vector exists. The model
245
- reports that rather than dividing by the residue. The raw signed values are
246
- still returned — they are diagnostics on the game's own scale, not shares.
252
+ Each converting journey splits its conversion equally among the distinct
253
+ channels it touched, so the raw values sum to the seven observed conversions
254
+ and the shares are those values divided by seven. `attribution` is `None`
255
+ when normalization is unavailable — for the default game, only when no
256
+ conversion can be credited to a channel — so code that must handle that case
257
+ should check `normalization_status` before indexing it.
247
258
 
248
259
  Lists, NumPy arrays, and pandas Series all work as path and conversion
249
- inputs.
260
+ inputs. Both must be one-dimensional: a bare string, a scalar, or a nested or
261
+ two-dimensional container raises instead of being read as a collection of
262
+ journeys. Each conversion outcome must be exactly `0` or `1`. Integers,
263
+ booleans, and the exact numeric values `0.0` / `1.0` are accepted in any of
264
+ those containers. The canonical strings `"0"` and `"1"` (text or bytes)
265
+ remain accepted as well; no other spelling (`" 1"`, `"01"`, `"1.0"`,
266
+ `"true"`) is. Every `fit` entry point — static, temporal (`fit` and
267
+ `fit_by_epoch`), and `CompareAttribution` — and the `validate_data` /
268
+ `to_int_array` helpers check the supplied values before integer conversion
269
+ and raise a `ValueError` naming the position and the original value for a
270
+ fractional, out-of-range, missing (`None`, `NaN`, `pd.NA`), or non-finite
271
+ outcome.
272
+
273
+ > **Compatibility — binary outcomes:** this enforces the existing
274
+ > binary-outcome contract. Earlier releases cast outcomes to integers before
275
+ > checking them, so `[0.9, 1.9]` was silently fitted as `[0, 1]`. Callers
276
+ > passing probabilities or fractional counts must supply valid `0`/`1`
277
+ > journey outcomes rather than relying on truncation.
250
278
 
251
279
  ---
252
280
 
@@ -263,6 +291,19 @@ identifies candidate temporal boundaries.
263
291
  | <img src="https://raw.githubusercontent.com/8139CAUSAL/do_attribution/main/examples/images/ga4_model_comparison.png" width="420" alt="GA4 Markov vs Shapley comparison chart"> | <img src="https://raw.githubusercontent.com/8139CAUSAL/do_attribution/main/examples/images/ga4_markov_epoch_recommendation.png" width="420" alt="GA4 epoch recommendation diagnostic plot"> |
264
292
  | `plot_attribution_comparison` — Markov and Shapley attribution on the same journeys | `plot_epoch_recommendation` — candidate and retained epoch boundaries on the conversion-rate signal |
265
293
 
294
+ The calibration example starts from the experiment. One figure follows the
295
+ randomized outcomes into the calibration: what each arm converted, the
296
+ estimated lift with its 95% confidence interval beside the matched attribution
297
+ estimate, and the calibration ratio of each epoch. The December ratio is
298
+ marked as interpolated because no experiment ran in that month. The interval
299
+ belongs to the experimental lift only; calibration does not propagate the
300
+ standard error, so no ratio and no calibrated estimate carries one.
301
+
302
+ | |
303
+ | :---: |
304
+ | <img src="https://raw.githubusercontent.com/8139CAUSAL/do_attribution/main/examples/images/calibration_experimental_evidence.png" width="860" alt="Experimental lift evidence and its translation into attribution calibration: arm outcomes, lift with 95% confidence interval versus matched MTA estimate, and calibration ratios across time"> |
305
+ | `plot_experimental_evidence` in `examples/calibration_workflow.py` — randomized outcomes, estimated lift versus matched attribution, and observed versus interpolated calibration ratios |
306
+
266
307
  Calibration diagnostics keep experimental evidence, constructed calibration
267
308
  ratios, direct adjustments, and policy-based residual allocation visually
268
309
  distinct.
@@ -271,8 +312,8 @@ distinct.
271
312
  | :---: | :---: |
272
313
  | <img src="https://raw.githubusercontent.com/8139CAUSAL/do_attribution/main/examples/images/calibration_static_effects.png" width="420" alt="Static calibration effects"> | <img src="https://raw.githubusercontent.com/8139CAUSAL/do_attribution/main/examples/images/calibration_temporal_epoch_alignment.png" width="420" alt="Temporal epoch and experiment alignment"> |
273
314
  | Decomposes the calibrated estimate into original attribution, direct test adjustment, and residual allocation | Shows where experimental evidence falls on caller-defined epochs without redefining the epoch boundaries |
274
- | <img src="https://raw.githubusercontent.com/8139CAUSAL/do_attribution/main/examples/images/calibration_temporal_ratio_schedule.png" width="420" alt="Temporal ratio schedule"> | <img src="https://raw.githubusercontent.com/8139CAUSAL/do_attribution/main/examples/images/calibration_temporal_residual_allocation.png" width="420" alt="Temporal residual allocation"> |
275
- | Distinguishes observed experimental anchors from constructed epoch-level calibration ratios | Shows the required residual adjustment and its policy-based allocation across eligible recipients |
315
+ | <img src="https://raw.githubusercontent.com/8139CAUSAL/do_attribution/main/examples/images/calibration_temporal_residual_allocation.png" width="420" alt="Temporal residual allocation"> | |
316
+ | Shows the required residual adjustment and its policy-based allocation across eligible recipients | |
276
317
 
277
318
  ---
278
319
 
@@ -295,6 +336,28 @@ residual allocation, or calibrated total.
295
336
  Calibrated point outputs therefore do not supply confidence intervals,
296
337
  posterior uncertainty, or uncertainty-adjusted calibration ratios.
297
338
 
339
+ The standard error is displayed wherever the evidence itself is displayed,
340
+ as the experiment's reported uncertainty about its own observed lift:
341
+
342
+ - `summary()` (static and temporal) prints an
343
+ `Observed lift ± standard error` line under each evidence record, for
344
+ example `360.00 ± 25.00 (standard error reported by the experiment;
345
+ calibration does not use it)`. A record without one prints
346
+ `360.00; no uncertainty supplied`.
347
+ - The temporal `to_dataframes()["evidence"]` table carries `observed_lift`,
348
+ `standard_error`, and `uncertainty_status` beside each record;
349
+ `uncertainty_status` reads `standard error reported by the experiment` or
350
+ `no uncertainty supplied`, so a missing value is never an implied zero.
351
+ - `plot_evidence_uncertainty()` draws each evidence record's observed lift
352
+ on a lift axis with a horizontal ±1 standard-error bar, and labels a record
353
+ without one `no uncertainty supplied`. `plot_ratio_schedule()` draws the
354
+ same reported error on its observed anchors, divided by the matched
355
+ attribution estimate so it sits on the ratio axis. Constructed schedule
356
+ values, effective ratios, and calibrated estimates never carry a bar.
357
+
358
+ Changing `standard_error` changes those annotations only; every calibrated
359
+ number, schedule, and provenance record is identical.
360
+
298
361
  `AttributionCalibration` applies that evidence to caller-approved static
299
362
  attribution estimates. `TemporalAttributionCalibration` applies dated
300
363
  evidence to estimates on a caller-supplied, authoritative epoch grid; the
@@ -315,15 +378,15 @@ the calibration notebook:
315
378
  from do_attribution import AttributionCalibration, LiftTestEvidence
316
379
 
317
380
  attributed_conversions = {
318
- "Experimental Media": 1149.1519,
319
- "Organic Search": 760.3327,
320
- "Referral": 672.0069,
321
- "Direct": 591.4872,
322
- "Other": 193.9282,
323
- "Paid Search": 41.3767,
324
- "Organic Social": 7.8723,
325
- "Affiliate": 7.6895,
326
- "Email": 2.1544,
381
+ "Experimental Media": 1037.8436,
382
+ "Referral": 912.5517,
383
+ "Organic Search": 834.4344,
384
+ "Direct": 391.0936,
385
+ "Other": 198.3367,
386
+ "Paid Search": 40.5804,
387
+ "Email": 7.1016,
388
+ "Organic Social": 2.5363,
389
+ "Affiliate": 1.5218,
327
390
  }
328
391
 
329
392
  evidence = LiftTestEvidence(
@@ -412,12 +475,27 @@ legacy_refit["population_diagnostics"]["A"]["transformed_journey_count"] # 4
412
475
  Raw effects from `fail`, `fail_refit`, and `detour` are diagnostics rather
413
476
  than probabilities. Signed effects from `fail_refit` or `detour` do not
414
477
  necessarily define a probability distribution and must not be interpreted as
415
- percentages. The model normalizes raw effects only when they satisfy the
416
- shared finite, nonnegative, bounded, unit-sum contract. If signed effects
417
- cancel at or below the scale-aware tolerance, `attribution` is `None`;
478
+ percentages. Every strategy, `redirect_to_null` included, normalizes its raw
479
+ effects through the same shared contract, and only when they satisfy its
480
+ finite, nonnegative, bounded, unit-sum rules. `fit` always returns
418
481
  `normalization_status`, `normalization_reason`, and the raw-sum, magnitude,
419
- denominator, and cancellation-ratio diagnostics explain why while
420
- `removal_effects` remain available.
482
+ denominator, and cancellation-ratio diagnostics. If signed effects cancel at
483
+ or below the scale-aware tolerance, or every effect is zero (no journey
484
+ converts, or stripping `conversion_labels` leaves no converting journey with
485
+ a channel), `attribution` is `None` and those diagnostics explain why while
486
+ `removal_effects` and `total_conversion_probability` remain available.
487
+
488
+ > **Compatibility — `redirect_to_null`:** earlier releases substituted an
489
+ > equal share per channel, with a `Total removal effect is 0` warning, when
490
+ > every removal effect was zero, and did not return the normalization
491
+ > diagnostics. Such a fit now returns `attribution=None` with
492
+ > `normalization_reason="zero_raw_magnitude"` and no warning; temporal
493
+ > epochs and `CompareAttribution` columns built from it hold missing shares
494
+ > instead of equal ones. Callers that assumed `redirect_to_null` always
495
+ > returned a dictionary need the availability check the other strategies
496
+ > already require: test `normalization_status` (or `attribution is None`)
497
+ > before indexing. Removal-effect mathematics, and the shares of any fit
498
+ > with a nonzero effect, are unchanged.
421
499
 
422
500
  Higher orders (`order=2`, `order=3`, …) use sliding tuples as states; short
423
501
  paths are left-padded with `START`. `fit` returns `attribution`,
@@ -426,36 +504,64 @@ paths are left-padded with `START`. `fit` returns `attribution`,
426
504
 
427
505
  ### Shapley coalition attribution
428
506
 
429
- `ShapleyAttribution` uses the value function *v(S) = conversions on paths
430
- whose unique channel set is exactly S* and exact factorial weights. `fit`
431
- returns `attribution`, raw signed `shapley_values`, and the observed
432
- `value_function`:
507
+ `ShapleyAttribution` builds a coalition game from the observed coalition
508
+ table *R(T) = conversions on paths whose unique channel set is exactly T*,
509
+ which `fit` returns as `value_function` alongside `attribution` and the raw
510
+ `shapley_values`:
433
511
 
434
512
  ```python
435
513
  shapley["value_function"]
436
514
  # {('Email', 'Social'): 2, ('Email', 'Search'): 1, ('Social',): 1, ...}
437
515
  ```
438
516
 
439
- Raw signed `shapley_values` and normalized `attribution` are **separate
440
- outputs with separate contracts**:
517
+ `characteristic_function=` selects the game *v* the Shapley values are
518
+ computed on:
519
+
520
+ | Game | *v(S)* | Efficiency: the raw values sum to | Raw values |
521
+ | --- | --- | --- | --- |
522
+ | `"cumulative"` (default) | Conversions on paths whose channel set is contained in *S*: the sum of *R(T)* over observed *T ⊆ S*. | Every observed conversion. | Non-negative. Each path splits its conversion equally among its distinct channels: `phi_i = Σ R(T) / |T|` over the coalitions *T* containing *i*. |
523
+ | `"exact"` (legacy) | *R(S)*: conversions on paths whose channel set is exactly *S*. | The conversions on paths that touch every channel, `v(N)`. | Signed. |
524
+
525
+ The cumulative game is monotone and its grand coalition is worth the total
526
+ number of conversions, so its Shapley values are an allocation of the
527
+ conversions that were actually observed. The closed form above is exactly
528
+ the factorial-weight Shapley definition applied to that game; it is
529
+ evaluated in one pass over the observed coalitions rather than over the
530
+ `2^n` coalition lattice.
531
+
532
+ Raw `shapley_values` and normalized `attribution` are **separate outputs
533
+ with separate contracts**:
441
534
 
442
535
  | Output | Contract |
443
536
  | --- | --- |
444
- | `shapley_values` | Always present. Signed, on the game's own scale. Not shares, percentages, or probabilities. |
537
+ | `shapley_values` | Always present, on the game's own scale (conversions), never rescaled. Under `"cumulative"` they are non-negative and sum to the observed conversions. Under `"exact"` they are signed diagnostics — not shares, percentages, or probabilities. |
445
538
  | `attribution` | A share mapping when available, otherwise `None`. Every value is finite and in `[0, 1]`, and the values sum to 1. |
446
539
 
447
- Because no journey may touch every channel, the efficiency axiom forces the
448
- raw values to sum to `v(N) = 0` by construction. Dividing by that float
449
- residue produces numerically explosive output, so normalization is refused
450
- instead: `attribution` is `None`, a warning is emitted, and the fit still
451
- succeeds with its raw values and diagnostics intact.
540
+ ```python
541
+ shapley["characteristic_function"] # 'cumulative'
542
+ shapley["normalization_status"] # 'available'
543
+ shapley["raw_sum"] # 7.0 — the observed conversions
544
+ shapley["cancellation_ratio"] # 1.0
545
+ ```
546
+
547
+ Under the legacy game, when no journey touches every channel the efficiency
548
+ axiom forces the raw values to sum to `v(N) = 0` by construction. Dividing
549
+ by that float residue produces numerically explosive output, so
550
+ normalization is refused instead: `attribution` is `None`, a warning is
551
+ emitted, and the fit still succeeds with its raw values and diagnostics
552
+ intact.
452
553
 
453
554
  ```python
454
- shapley["normalization_status"] # 'available' | 'unavailable'
455
- shapley["normalization_reason"] # e.g. 'near_cancellation'
456
- shapley["raw_sum"] # ~1e-17 — float residue, not a real total
457
- shapley["raw_magnitude"] # 1.3333
458
- shapley["cancellation_ratio"] # ~1e-17 — far below the 1e-12 threshold
555
+ legacy = ShapleyAttribution(
556
+ conversion_labels=["Purchase"], characteristic_function="exact"
557
+ ).fit(journeys, conversions)
558
+
559
+ legacy["normalization_status"] # 'unavailable'
560
+ legacy["normalization_reason"] # 'near_cancellation'
561
+ legacy["shapley_values"] # {'Direct': -0.5833, 'Email': 0.25, 'Search': -0.0833, 'Social': 0.4167}
562
+ legacy["raw_sum"] # ~1e-17 — float residue, not a real total
563
+ legacy["raw_magnitude"] # 1.3333
564
+ legacy["cancellation_ratio"] # ~1e-17 — far below the 1e-12 threshold
459
565
  ```
460
566
 
461
567
  The exact residue in `raw_sum` is not reproducible across summation orders,
@@ -471,6 +577,50 @@ codes are `zero_raw_magnitude`, `near_cancellation`, `nonfinite_result`,
471
577
  Attributed conversions are only defined when `attribution` is available;
472
578
  they are never derived from raw signed values.
473
579
 
580
+ #### Observation weights
581
+
582
+ `ShapleyAttribution.fit(..., weights=)` takes optional per-journey
583
+ observation weights — frequencies when one row stands for several identical
584
+ journeys, or survey and sampling weights. The coalition table becomes
585
+ *R_w(T) = Σ wⱼ·Yⱼ over the journeys whose channel set is exactly T*, and
586
+ both characteristic functions are evaluated on it as they are on *R(T)*.
587
+ Weighted totals are floats and are never truncated to integers. Without
588
+ `weights` nothing changes: the table holds integer counts.
589
+
590
+ ```python
591
+ weights = [2.5, 1.0, 1.0, 4.0, 0.5, 0.0, 2.0, 1.5, 1.0, 3.0]
592
+
593
+ weighted_model = ShapleyAttribution(conversion_labels=["Purchase"])
594
+ weighted = weighted_model.fit(journeys, conversions, weights=weights)
595
+
596
+ weighted["value_function"]
597
+ # {('Email', 'Social'): 3.5, ('Email', 'Search'): 1.0, ('Social',): 1.0, ...}
598
+ print({ch: round(v, 4) for ch, v in weighted["shapley_values"].items()})
599
+ # {'Email': 2.9167, 'Search': 1.1667, 'Social': 3.4167}
600
+ print({ch: round(share, 4) for ch, share in weighted["attribution"].items()})
601
+ # {'Email': 0.3889, 'Search': 0.1556, 'Social': 0.4556}
602
+ weighted["raw_sum"] # 7.5 — the weighted conversions
603
+ ```
604
+
605
+ Weights follow the `MarkovAttribution.fit(weights=)` contract: one-dimensional,
606
+ aligned with the journeys by position (the index labels of a pandas Series
607
+ are ignored), finite, nonnegative, and with a positive total. Booleans count
608
+ as `0` / `1`. A journey with zero weight is excluded before the channel list
609
+ and the coalition table are built, so a channel seen only on zero-weight
610
+ journeys — `Direct` above — is absent from every output. Standalone
611
+ `MarkovAttribution` does not drop zero-weight journeys from its channel
612
+ list. Integer frequency weights give the same result as repeating each
613
+ journey that many times.
614
+
615
+ Efficiency holds on the weighted table: under `"cumulative"` the raw values
616
+ sum to the weighted conversions of every nonempty coalition, and under
617
+ `"exact"` to `v_w(N)`. When no positively weighted journey converts,
618
+ `attribution` is `None` with `normalization_reason="zero_raw_magnitude"`.
619
+
620
+ [`examples/weighted_attribution.ipynb`](https://github.com/8139CAUSAL/do_attribution/blob/main/examples/weighted_attribution.ipynb)
621
+ works through frequency weights, fractional and zero weights, and a weighted
622
+ comparison on the GA4 journey extract.
623
+
474
624
  ### Side-by-side comparison
475
625
 
476
626
  ```python
@@ -479,21 +629,73 @@ from do_attribution import CompareAttribution
479
629
  comparison = CompareAttribution(conversion_labels=["Purchase"])
480
630
  comparison.fit(journeys, conversions)
481
631
 
482
- comparison.attribution_frame() # markov shapley
483
- # Direct 0.026316 NaN
484
- # Email 0.394737 NaN
485
- # Search 0.201754 NaN
486
- # Social 0.377193 NaN
487
- comparison.unavailable_models() # {'shapley': 'near_cancellation'}
632
+ comparison.attribution_frame() # markov shapley
633
+ # Direct 0.071429 0.142857
634
+ # Email 0.357143 0.309524
635
+ # Search 0.214286 0.166667
636
+ # Social 0.357143 0.380952
637
+ comparison.unavailable_models() # {} — every model produced a share vector
488
638
  comparison.difference_frame("markov")
489
639
  ```
490
640
 
641
+ A model whose normalization is unavailable contributes an all-`NaN` column
642
+ and is listed by `unavailable_models()` with its reason code.
643
+ `shapley_characteristic_function=` selects the Shapley game.
644
+
491
645
  The two methods answer different statistical questions and can disagree
492
646
  materially without either being defective — `difference_frame` makes the
493
- disagreement explicit. `extra_models={...}` plugs any additional
494
- `BaseAttribution` implementation into the same aligned tables, so
495
- first-touch, last-touch, or any custom baseline can sit beside the two
496
- built-in models.
647
+ disagreement explicit.
648
+
649
+ `include_baselines=True` adds the two built-in single-touch baselines as
650
+ fixed reference points, so that disagreement can be read against the naive
651
+ allocations practitioners actually use. `FirstTouchAttribution` gives each
652
+ converting journey's whole conversion to the first channel it touched,
653
+ `LastTouchAttribution` to the last. Both are `BaseAttribution` models with
654
+ the same `fit` / `get_attribution` / `summary` API, the same validation of
655
+ journeys and binary outcomes, the same `conversion_labels` handling, and the
656
+ same normalization contract: `attribution` is `None`, with
657
+ `normalization_reason="zero_raw_magnitude"`, when no conversion can be
658
+ credited to a channel. The raw counts are returned as
659
+ `credited_conversions`.
660
+
661
+ ```python
662
+ comparison = CompareAttribution(
663
+ conversion_labels=["Purchase"], include_baselines=True
664
+ )
665
+ comparison.fit(journeys, conversions)
666
+
667
+ comparison.attribution_frame() # markov shapley first_touch last_touch
668
+ # Direct 0.071429 0.142857 0.142857 0.142857
669
+ # Email 0.357143 0.309524 0.142857 0.428571
670
+ # Search 0.214286 0.166667 0.428571 0.000000
671
+ # Social 0.357143 0.380952 0.285714 0.428571
672
+ comparison.difference_frame("last_touch")
673
+ ```
674
+
675
+ The baselines are opt-in: without `include_baselines=True` the comparison
676
+ holds only the `markov` and `shapley` columns, exactly as before. The
677
+ classes can also be used on their own or passed through `extra_models`.
678
+ `extra_models={...}` plugs any additional `BaseAttribution` implementation
679
+ into the same aligned tables, so a custom baseline can sit beside the
680
+ built-in ones.
681
+
682
+ `CompareAttribution.fit(..., weights=)` runs a weighted comparison.
683
+ Zero-weight journeys are dropped first, and the remaining journeys and
684
+ weights are passed to both models, so Markov and Shapley are always fitted
685
+ on the same weighted population and neither is fitted unweighted. The
686
+ baselines and `extra_models` do not accept weights: a weighted fit on a
687
+ comparison built with `include_baselines=True` or with `extra_models` raises
688
+ `ValueError` before any model is fitted.
689
+
690
+ ```python
691
+ comparison = CompareAttribution(conversion_labels=["Purchase"])
692
+ comparison.fit(journeys, conversions, weights=weights)
693
+
694
+ comparison.attribution_frame() # markov shapley
695
+ # Email 0.406250 0.388889
696
+ # Search 0.187500 0.155556
697
+ # Social 0.406250 0.455556
698
+ ```
497
699
 
498
700
  ### Conversion-endpoint declaration
499
701
 
@@ -577,11 +779,48 @@ diagnostics) with the complete grids available programmatically via
577
779
  `to_dataframes()` and the `get_*` accessors. The reporting language is
578
780
  descriptive; it never labels a movement statistically significant or causal.
579
781
 
782
+ `MarkovAttribution` and `ShapleyAttribution` export their last successful
783
+ fit with the same `to_dataframes()` call the temporal models use. Each
784
+ returns a channel/share table (`attribution`), the raw-value table
785
+ (`removal_effects` or `shapley_values`), and a one-row `fit_diagnostics`
786
+ table holding the model settings and the normalization status, reason, and
787
+ numeric diagnostics. The Markov export also carries `conversion_probability`
788
+ in that row and a flat `transition_matrix` edge table (`from_state`,
789
+ `to_state`, `probability`), which is empty with the same columns unless the
790
+ model was built with `return_transition_matrix=True`. When normalization is
791
+ unavailable the channel rows stay in `attribution` with a missing share, so
792
+ an absent share is never written as zero credit, while the raw-value table
793
+ keeps the signed diagnostics. The tables are copies of a snapshot taken at
794
+ fit time: exporting never refits, and editing a returned frame changes
795
+ neither the model nor a later export. Calling it before `fit` raises
796
+ `RuntimeError`.
797
+
798
+ The Shapley `fit_diagnostics` row ends with three observation-weight
799
+ columns: `weights_supplied`, `total_weight` (the sum of the weights), and
800
+ `weighted_conversions` (the sum of weight × outcome over every supplied
801
+ journey). An unweighted fit records `False`, its journey count, and its
802
+ conversion count. In `summary()`, the journey count, conversion count, and
803
+ conversion rate stay plain counts of the rows passed to `fit`; a weighted
804
+ fit adds an `Observation weights` section with the weighted totals, and its
805
+ coalition tables show weighted conversions without rounding them to
806
+ integers. `Path diagnostics` likewise counts rows rather than weights, over
807
+ the rows with positive weight. With frequency weights these row counts
808
+ describe the rows of the frequency table, not the journeys they stand for.
809
+
810
+ ```python
811
+ tables = model.to_dataframes() # `model` is the quick-start MarkovAttribution
812
+ list(tables)
813
+ # ['attribution', 'removal_effects', 'fit_diagnostics', 'transition_matrix']
814
+
815
+ for name, table in tables.items():
816
+ table.to_csv(f"markov_{name}.csv", index=False)
817
+ ```
818
+
580
819
  ---
581
820
 
582
821
  ## Detailed examples
583
822
 
584
- All six notebooks run end to end on the same GA4 journey extract
823
+ All seven notebooks run end to end on the same GA4 journey extract
585
824
  ([`examples/ga4_attribution_journeys.csv`](https://github.com/8139CAUSAL/do_attribution/blob/v0.0.2/examples/ga4_attribution_journeys.csv))
586
825
  — a transformed journey-level
587
826
  extract created from
@@ -594,9 +833,10 @@ path, a binary conversion flag, and an anchor timestamp.
594
833
  | [`examples/basic_markov.ipynb`](https://github.com/8139CAUSAL/do_attribution/blob/v0.0.2/examples/basic_markov.ipynb) | static Markov fit, shares, removal effects, summary, transition graph |
595
834
  | [`examples/basic_shapley.ipynb`](https://github.com/8139CAUSAL/do_attribution/blob/v0.0.2/examples/basic_shapley.ipynb) | Shapley shares, raw values, coalition results, coalition plot |
596
835
  | [`examples/side_by_side.ipynb`](https://github.com/8139CAUSAL/do_attribution/blob/v0.0.2/examples/side_by_side.ipynb) | `CompareAttribution` aligned tables, differences, comparison plot |
836
+ | [`examples/weighted_attribution.ipynb`](https://github.com/8139CAUSAL/do_attribution/blob/main/examples/weighted_attribution.ipynb) | optional `weights=`: frequency weights that reproduce the one-row-per-journey Shapley fit, fractional and zero weights, the weighted summary and export, and a weighted `CompareAttribution` on a down-sampled journey table |
597
837
  | [`examples/temporal_markov.ipynb`](https://github.com/8139CAUSAL/do_attribution/blob/v0.0.2/examples/temporal_markov.ipynb) | weekly Markov epochs, diagnostics, `to_dataframes()`, epoch recommendation |
598
838
  | [`examples/temporal_shapley.ipynb`](https://github.com/8139CAUSAL/do_attribution/blob/v0.0.2/examples/temporal_shapley.ipynb) | weekly Shapley epochs, channel trends, diagnostics, epoch recommendation |
599
- | [`examples/attribution_experiment_calibration.ipynb`](https://github.com/8139CAUSAL/do_attribution/blob/v0.0.2/examples/attribution_experiment_calibration.ipynb) | seeded semi-synthetic randomized campaign on the GA4 rows, matched static and temporal Markov estimates, calibration, and seven diagnostics |
839
+ | [`examples/attribution_experiment_calibration.ipynb`](https://github.com/8139CAUSAL/do_attribution/blob/v0.0.2/examples/attribution_experiment_calibration.ipynb) | seeded semi-synthetic randomized campaign on the GA4 rows, matched static and temporal Markov estimates, calibration, the experimental-evidence figure, and seven calibration diagnostics |
600
840
 
601
841
 
602
842
  ---