do-attribution 0.0.1.post1__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 (55) hide show
  1. do_attribution-0.0.1.post1/LICENSE +21 -0
  2. do_attribution-0.0.1.post1/MANIFEST.in +19 -0
  3. do_attribution-0.0.1.post1/PKG-INFO +411 -0
  4. do_attribution-0.0.1.post1/README.md +371 -0
  5. do_attribution-0.0.1.post1/do_attribution/__init__.py +115 -0
  6. do_attribution-0.0.1.post1/do_attribution/base.py +147 -0
  7. do_attribution-0.0.1.post1/do_attribution/compare.py +138 -0
  8. do_attribution-0.0.1.post1/do_attribution/markov.py +502 -0
  9. do_attribution-0.0.1.post1/do_attribution/reporting.py +822 -0
  10. do_attribution-0.0.1.post1/do_attribution/shapley.py +244 -0
  11. do_attribution-0.0.1.post1/do_attribution/temporal.py +1546 -0
  12. do_attribution-0.0.1.post1/do_attribution/temporal_diagnostics.py +214 -0
  13. do_attribution-0.0.1.post1/do_attribution/temporal_markov.py +1509 -0
  14. do_attribution-0.0.1.post1/do_attribution/temporal_shapley.py +1383 -0
  15. do_attribution-0.0.1.post1/do_attribution/utils.py +299 -0
  16. do_attribution-0.0.1.post1/do_attribution/visualization.py +333 -0
  17. do_attribution-0.0.1.post1/do_attribution.egg-info/PKG-INFO +411 -0
  18. do_attribution-0.0.1.post1/do_attribution.egg-info/SOURCES.txt +53 -0
  19. do_attribution-0.0.1.post1/do_attribution.egg-info/dependency_links.txt +1 -0
  20. do_attribution-0.0.1.post1/do_attribution.egg-info/requires.txt +21 -0
  21. do_attribution-0.0.1.post1/do_attribution.egg-info/top_level.txt +1 -0
  22. do_attribution-0.0.1.post1/examples/basic_markov.ipynb +755 -0
  23. do_attribution-0.0.1.post1/examples/basic_shapley.ipynb +807 -0
  24. do_attribution-0.0.1.post1/examples/ga4_attribution_journeys.csv +272793 -0
  25. do_attribution-0.0.1.post1/examples/images/ga4_coalition_values.png +0 -0
  26. do_attribution-0.0.1.post1/examples/images/ga4_markov_attribution.png +0 -0
  27. do_attribution-0.0.1.post1/examples/images/ga4_markov_epoch_recommendation.png +0 -0
  28. do_attribution-0.0.1.post1/examples/images/ga4_markov_removal_effects.png +0 -0
  29. do_attribution-0.0.1.post1/examples/images/ga4_model_comparison.png +0 -0
  30. do_attribution-0.0.1.post1/examples/images/ga4_shapley_attribution.png +0 -0
  31. do_attribution-0.0.1.post1/examples/images/ga4_shapley_epoch_recommendation.png +0 -0
  32. do_attribution-0.0.1.post1/examples/images/ga4_temporal_markov.png +0 -0
  33. do_attribution-0.0.1.post1/examples/images/ga4_temporal_shapley.png +0 -0
  34. do_attribution-0.0.1.post1/examples/images/ga4_transition_graph.png +0 -0
  35. do_attribution-0.0.1.post1/examples/side_by_side.ipynb +529 -0
  36. do_attribution-0.0.1.post1/examples/temporal_markov.ipynb +1538 -0
  37. do_attribution-0.0.1.post1/examples/temporal_shapley.ipynb +1571 -0
  38. do_attribution-0.0.1.post1/pyproject.toml +65 -0
  39. do_attribution-0.0.1.post1/requirements.txt +8 -0
  40. do_attribution-0.0.1.post1/setup.cfg +4 -0
  41. do_attribution-0.0.1.post1/setup.py +58 -0
  42. do_attribution-0.0.1.post1/tests/__init__.py +0 -0
  43. do_attribution-0.0.1.post1/tests/conftest.py +78 -0
  44. do_attribution-0.0.1.post1/tests/test_compare.py +110 -0
  45. do_attribution-0.0.1.post1/tests/test_markov.py +476 -0
  46. do_attribution-0.0.1.post1/tests/test_shapley.py +277 -0
  47. do_attribution-0.0.1.post1/tests/test_temporal_diagnostics.py +214 -0
  48. do_attribution-0.0.1.post1/tests/test_temporal_markov.py +274 -0
  49. do_attribution-0.0.1.post1/tests/test_temporal_recommendation.py +325 -0
  50. do_attribution-0.0.1.post1/tests/test_temporal_reporting.py +195 -0
  51. do_attribution-0.0.1.post1/tests/test_temporal_shapley.py +249 -0
  52. do_attribution-0.0.1.post1/tests/test_temporal_v2_mixed_freq.py +456 -0
  53. do_attribution-0.0.1.post1/tests/test_to_dataframes.py +434 -0
  54. do_attribution-0.0.1.post1/tests/test_utils.py +236 -0
  55. do_attribution-0.0.1.post1/tests/test_visualization.py +280 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Aaron Goodin
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,19 @@
1
+ # Files to include in the PyPI source distribution (sdist).
2
+ # The wheel is built from the `do_attribution` package only.
3
+
4
+ include LICENSE
5
+ include README.md
6
+ include requirements.txt
7
+ include pyproject.toml
8
+ include setup.py
9
+
10
+ recursive-include tests *.py
11
+ recursive-include examples *.ipynb *.csv
12
+ recursive-include examples/images *.png
13
+ recursive-include docs *.md
14
+
15
+ # Never ship cruft.
16
+ global-exclude .DS_Store
17
+ global-exclude __pycache__
18
+ global-exclude *.py[cod]
19
+ global-exclude *.so
@@ -0,0 +1,411 @@
1
+ Metadata-Version: 2.4
2
+ Name: do-attribution
3
+ Version: 0.0.1.post1
4
+ Summary: Unified multi-touch attribution library (Markov chain + Shapley value).
5
+ Author: Aaron Goodin
6
+ Author-email: Aaron Goodin <goodinaaron@causalscience.com>
7
+ License: MIT
8
+ Keywords: attribution,marketing,markov,shapley,multi-touch,analytics,incrementality
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Programming Language :: Python :: 3.10
11
+ Classifier: Programming Language :: Python :: 3.11
12
+ Classifier: Programming Language :: Python :: 3.12
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Intended Audience :: Science/Research
16
+ Classifier: Topic :: Scientific/Engineering :: Information Analysis
17
+ Requires-Python: >=3.10
18
+ Description-Content-Type: text/markdown
19
+ License-File: LICENSE
20
+ Requires-Dist: numpy>=1.21.0
21
+ Requires-Dist: pandas>=1.3.0
22
+ Provides-Extra: viz
23
+ Requires-Dist: matplotlib>=3.3.0; extra == "viz"
24
+ Requires-Dist: networkx>=2.5; extra == "viz"
25
+ Provides-Extra: temporal
26
+ Requires-Dist: statsmodels>=0.14; extra == "temporal"
27
+ Requires-Dist: ruptures>=1.1; extra == "temporal"
28
+ Provides-Extra: excel
29
+ Requires-Dist: openpyxl>=3.0.0; extra == "excel"
30
+ Provides-Extra: dev
31
+ Requires-Dist: pytest>=7.0; extra == "dev"
32
+ Requires-Dist: matplotlib>=3.3.0; extra == "dev"
33
+ Requires-Dist: networkx>=2.5; extra == "dev"
34
+ Requires-Dist: openpyxl>=3.0.0; extra == "dev"
35
+ Requires-Dist: statsmodels>=0.14; extra == "dev"
36
+ Requires-Dist: ruptures>=1.1; extra == "dev"
37
+ Dynamic: author
38
+ Dynamic: license-file
39
+ Dynamic: requires-python
40
+
41
+ # do-attribution
42
+
43
+ **Multi-touch attribution for Python — closed-form Markov and exact Shapley,
44
+ with static and epoch-based workflows behind one package.**
45
+
46
+ ![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)
47
+ ![Python: 3.10+](https://img.shields.io/badge/python-3.10%2B-blue.svg)
48
+
49
+ You have customer journeys that look like `"Email > Social > Purchase"` and a
50
+ `0/1` conversion flag per journey. `do-attribution` turns those journeys into
51
+ per-channel shares of the observed conversions using two statistical
52
+ allocation models:
53
+
54
+ - an **absorbing-Markov-chain removal-effect** model
55
+ (`MarkovAttribution`, orders 1+, three removal strategies), and
56
+ - a **cooperative-game Shapley-value** model (`ShapleyAttribution`, exact
57
+ factorial weights over the observed coalition lattice).
58
+
59
+ Around the two models the package ships side-by-side model comparison
60
+ (`CompareAttribution`), explicit conversion-endpoint handling
61
+ (`conversion_labels`), plain-text reporting (`summary()`), flat DataFrame
62
+ export (`to_dataframes()`), matplotlib/networkx visualizations, and temporal
63
+ epoch analysis (`TemporalMarkovAttribution`, `TemporalShapleyAttribution`,
64
+ `recommend_epochs`) that refits the same static models independently inside
65
+ chronological epochs.
66
+
67
+ > **Scope guard:** these are probabilistic decompositions of observed
68
+ > co-occurrence between channel exposure and conversion — **not causal
69
+ > inference**. A high share means the channel is statistically associated
70
+ > with converting paths under the model's assumptions. It does not identify
71
+ > the channel that caused a conversion, and it does not replace
72
+ > incrementality tests or randomized experiments.
73
+
74
+ ---
75
+
76
+ ## Key capabilities
77
+
78
+ - **Deterministic, closed-form Markov attribution.** Removal effects come
79
+ analytically from the absorbing-chain fundamental matrix `(I − Q)⁻¹` — no
80
+ Monte Carlo, no seed, no simulation count. Identical inputs always produce
81
+ identical outputs.
82
+ - **Three removal-effect definitions.** `removal_strategy=` `"fail"`,
83
+ `"detour"` (signed effects), or `"redirect_to_null"` — three operational
84
+ meanings of "remove this channel", selected explicitly instead of implied.
85
+ - **Exact Shapley values.** Per-channel values use the exact factorial weight
86
+ `w(s, n) = s!·(n−s−1)!/n!`; raw signed values, the coalition value
87
+ function, and normalized shares are all returned.
88
+ - **Conservation and contract guarantees.** Attribution shares sum to 1,
89
+ attributed conversions conserve the observed total, `'fail'` removal
90
+ effects are non-negative, and absorption probabilities stay on `[0, 1]` —
91
+ enforced by the implementation.
92
+ - **Model reporting.** Every fitted model renders a compact ASCII `summary()`
93
+ or a natural-language `summary(output_format="report")` — plain Python
94
+ strings with bounded, distilled detail tables (signed removal effects, top
95
+ transitions and drop-offs, top coalitions, path diagnostics).
96
+ - **Flat DataFrame export.** Temporal models expose
97
+ `to_dataframes() -> dict[str, pd.DataFrame]`: flat, string-keyed,
98
+ Parquet/SQL-ready tables for attribution, raw model values, and sample
99
+ diagnostics — no text parsing, no `MultiIndex`, no lazy refits.
100
+ - **Visualization.** `plot_attribution`, `plot_removal_effects`,
101
+ `plot_transition_graph`, `plot_coalition_values`,
102
+ `plot_attribution_comparison`, and `plot_epoch_recommendation` each return
103
+ a `matplotlib.figure.Figure`.
104
+ - **Temporal epoch analysis.** Calendar buckets (`freq=`), explicit labels
105
+ (`fit_by_epoch`), or data-driven boundaries
106
+ (`recommend_epochs` + `fit_recommended`) — every epoch is an independent
107
+ static fit; complete paths are never split across epochs.
108
+ - **Reproducible outputs.** Deterministic model fits, deterministic
109
+ renderers, and deterministic tie ordering — identical inputs always
110
+ produce identical reports, tables, and figures.
111
+
112
+ Core runtime dependencies are `numpy` and `pandas`.
113
+
114
+ ---
115
+
116
+ ## Installation
117
+
118
+ ```bash
119
+ pip install do-attribution
120
+ ```
121
+
122
+ | Extra | Installs | Enables |
123
+ | --- | --- | --- |
124
+ | `viz` | `matplotlib`, `networkx` | all plotting helpers |
125
+ | `temporal` | `statsmodels`, `ruptures` | global-signal epoch recommendation |
126
+ | `excel` | `openpyxl` | Excel-friendly workflows |
127
+ | `dev` | all of the above + `pytest` | running the test suite |
128
+
129
+ ```bash
130
+ pip install "do-attribution[viz]"
131
+ pip install "do-attribution[temporal,viz]"
132
+ ```
133
+
134
+ Requires Python **3.10 or newer**.
135
+
136
+ ---
137
+
138
+ ## Quick start
139
+
140
+ A complete, runnable analysis — ten journeys, a fitted Markov model, shares,
141
+ and a readable report:
142
+
143
+ ```python
144
+ from do_attribution import MarkovAttribution
145
+
146
+ journeys = [
147
+ "Email > Social > Purchase",
148
+ "Search > Email > Purchase",
149
+ "Social > Purchase",
150
+ "Email > Search", # no conversion
151
+ "Search > Social > Email > Purchase",
152
+ "Direct > Purchase",
153
+ "Email > Social", # no conversion
154
+ "Search > Email > Social > Purchase",
155
+ "Social > Email > Purchase",
156
+ "Search", # no conversion
157
+ ]
158
+ conversions = [1, 1, 1, 0, 1, 1, 0, 1, 1, 0]
159
+
160
+ model = MarkovAttribution(
161
+ order=1,
162
+ removal_strategy="redirect_to_null",
163
+ conversion_labels=["Purchase"], # endpoint token, not a channel
164
+ return_transition_matrix=True,
165
+ )
166
+ result = model.fit(journeys, conversions)
167
+
168
+ print({ch: round(share, 4) for ch, share in result["attribution"].items()})
169
+ print(f"P(conversion | start) = {result['total_conversion_probability']:.4f}")
170
+ print(model.summary())
171
+ ```
172
+
173
+ Output (abridged — the summary continues with top transitions, drop-off
174
+ bottlenecks, and path diagnostics):
175
+
176
+ ```text
177
+ {'Direct': 0.085, 'Email': 0.3912, 'Search': 0.1913, 'Social': 0.3324}
178
+ P(conversion | start) = 0.7000
179
+ Attribution Summary
180
+ Model: MarkovAttribution
181
+ Epoch source: static
182
+ Overall: 10 paths, 7 conversions, 70.00% conversion rate
183
+
184
+ Data
185
+ +-----------+-------+-------------+---------+--------------------------------------------+
186
+ | Cohort | Paths | Conversions | Conv. % | Top channels |
187
+ +-----------+-------+-------------+---------+--------------------------------------------+
188
+ | All paths | 10 | 7 | 70.00% | Email 39.12%, Social 33.24%, Search 19.13% |
189
+ +-----------+-------+-------------+---------+--------------------------------------------+
190
+
191
+ Removal effects (raw, signed) vs normalized shares
192
+ +---------+----------------+--------+
193
+ | Channel | Removal effect | Share |
194
+ +---------+----------------+--------+
195
+ | Email | +0.6571 | 39.12% |
196
+ | Social | +0.5584 | 33.24% |
197
+ | Search | +0.3214 | 19.13% |
198
+ | Direct | +0.1429 | 8.50% |
199
+ +---------+----------------+--------+
200
+ ```
201
+
202
+ The Shapley model is a drop-in alternative on the same inputs:
203
+
204
+ ```python
205
+ from do_attribution import ShapleyAttribution
206
+
207
+ shapley = ShapleyAttribution(conversion_labels=["Purchase"]).fit(journeys, conversions)
208
+ print({ch: round(share, 4) for ch, share in shapley["attribution"].items()})
209
+ # {'Direct': 0.0, 'Email': 0.375, 'Search': 0.0, 'Social': 0.625}
210
+ ```
211
+
212
+ Lists, NumPy arrays, and pandas Series all work as path and conversion
213
+ inputs.
214
+
215
+ ---
216
+
217
+ ## Visual examples
218
+
219
+ All figures below are outputs from the GA4 public ecommerce journey example
220
+ (`examples/ga4_attribution_journeys.csv` — 272,792 journeys, 8 channels,
221
+ Nov 2020 – Jan 2021), generated by the example notebooks with the shipped
222
+ plotting helpers.
223
+
224
+ | | |
225
+ | :---: | :---: |
226
+ | <img src="https://raw.githubusercontent.com/8139CAUSAL/do_attribution/main/examples/images/ga4_markov_attribution.png" width="420" alt="GA4 Markov attribution shares bar chart"> | <img src="https://raw.githubusercontent.com/8139CAUSAL/do_attribution/main/examples/images/ga4_transition_graph.png" width="420" alt="GA4 Markov transition graph"> |
227
+ | `plot_attribution` — Markov shares across the GA4 channels | `plot_transition_graph` — the fitted GA4 journey chain (edges ≥ 3%) |
228
+ | <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"> |
229
+ | `plot_attribution_comparison` — Markov vs Shapley on the same GA4 journeys | `plot_epoch_recommendation` — recommended epoch boundaries on the conversion-rate signal |
230
+
231
+ `plot_removal_effects` (signed bars) and `plot_coalition_values` (top Shapley
232
+ coalitions) complete the plotting surface; every helper returns a
233
+ `matplotlib.figure.Figure`. The full image bank lives in
234
+ [`examples/images/`](https://github.com/8139CAUSAL/do_attribution/tree/main/examples/images).
235
+
236
+ ---
237
+
238
+ ## Supported attribution methods
239
+
240
+ ### Markov removal-effect attribution
241
+
242
+ `MarkovAttribution(order=k)` models journeys as an absorbing Markov chain
243
+ (`START`, channels or sliding `k`-tuples of channels, `CONVERSION`, `NULL`)
244
+ and credits each channel by its removal effect, computed in closed form.
245
+ Three operational definitions of removal are provided:
246
+
247
+ | Strategy | Behavior |
248
+ | --- | --- |
249
+ | `"fail"` | Strip the channel from every path, force conversions on paths containing it to zero, rebuild the chain, recompute `P(CONVERSION \| START)`. Effects ≥ 0. |
250
+ | `"detour"` | Strip the channel while retaining original conversion flags, then refit. Effects are **signed** — a negative effect means removing the channel lowered modelled conversion probability. |
251
+ | `"redirect_to_null"` | Redirect the removed channel's transition mass to `NULL` on the fitted matrix and recompute absorption analytically — the classic removal-effect definition. |
252
+
253
+ Higher orders (`order=2`, `order=3`, …) use sliding tuples as states; short
254
+ paths are left-padded with `START`. `fit` returns `attribution`,
255
+ `removal_effects`, `total_conversion_probability`, and (opt-in) the
256
+ `transition_matrix`.
257
+
258
+ ### Shapley coalition attribution
259
+
260
+ `ShapleyAttribution` uses the value function *v(S) = conversions on paths
261
+ whose unique channel set is exactly S* and exact factorial weights. `fit`
262
+ returns `attribution`, raw signed `shapley_values`, and the observed
263
+ `value_function`:
264
+
265
+ ```python
266
+ shapley["value_function"]
267
+ # {('Email', 'Social'): 2, ('Email', 'Search'): 1, ('Social',): 1, ...}
268
+ ```
269
+
270
+ Because no journey may touch every channel, the raw values can sum to zero
271
+ by construction; normalization handles that regime deterministically (with a
272
+ warning) instead of producing unstable shares.
273
+
274
+ ### Side-by-side comparison
275
+
276
+ ```python
277
+ from do_attribution import CompareAttribution
278
+
279
+ comparison = CompareAttribution(conversion_labels=["Purchase"])
280
+ comparison.fit(journeys, conversions)
281
+
282
+ comparison.attribution_frame() # markov shapley
283
+ # Direct 0.0263 0.000
284
+ # Email 0.3947 0.375
285
+ # Search 0.2018 0.000
286
+ # Social 0.3772 0.625
287
+ comparison.difference_frame("markov")
288
+ ```
289
+
290
+ The two methods answer different statistical questions and can disagree
291
+ materially without either being defective — `difference_frame` makes the
292
+ disagreement explicit. `extra_models={...}` plugs any additional
293
+ `BaseAttribution` implementation into the same aligned tables, so
294
+ first-touch, last-touch, or any custom baseline can sit beside the two
295
+ built-in models.
296
+
297
+ ### Conversion-endpoint declaration
298
+
299
+ Terminal outcome tokens (`"Purchase"`, `"Sale"`, …) are declared, not
300
+ guessed: `conversion_labels=["Purchase"]` strips them before channel
301
+ extraction, state construction, coalition enumeration, reporting, and
302
+ plotting. Labels colliding with the reserved `START`/`CONVERSION`/`NULL`
303
+ tokens raise `ValueError`; `conversion_labels=None` treats every token as a
304
+ channel.
305
+
306
+ ### Temporal epoch analysis
307
+
308
+ `TemporalMarkovAttribution` and `TemporalShapleyAttribution` are
309
+ chronological orchestrators: each complete path is assigned to exactly one
310
+ epoch by a single anchor timestamp, each epoch gets an independent static
311
+ fit, and adjacent epochs share nothing — jumps at boundaries are valid
312
+ outputs of a discontinuous piecewise model, not smoothing artifacts.
313
+
314
+ ```python
315
+ import pandas as pd
316
+ from do_attribution import TemporalShapleyAttribution
317
+
318
+ anchors = pd.to_datetime([
319
+ "2025-01-05", "2025-01-12", "2025-02-08", "2025-02-17", "2025-04-03",
320
+ "2025-04-21", "2025-07-09", "2025-07-19", "2025-10-02", "2025-10-18",
321
+ ])
322
+
323
+ by_quarter = TemporalShapleyAttribution(
324
+ freq="Q", # calendar buckets; per-year mappings supported
325
+ min_samples_per_epoch=1, # demo-sized floor
326
+ conversion_labels=["Purchase"],
327
+ )
328
+ by_quarter.fit(journeys, anchors, conversions)
329
+
330
+ by_quarter.get_trend("Email") # per-epoch share for one channel
331
+ tables = by_quarter.to_dataframes() # flat DataFrames, ready for Parquet/SQL
332
+ list(tables)
333
+ # ['attribution', 'shapley_values', 'epoch_summary', 'channel_epoch_sample_sizes']
334
+ ```
335
+
336
+ The Markov variant additionally exports `removal_effects`,
337
+ `conversion_probability`, and `transition_matrices` tables. Three entry
338
+ points cover epoch selection:
339
+
340
+ - `fit(paths, timestamps, conversions)` — calendar frequency buckets;
341
+ - `fit_by_epoch(paths, epoch_labels, conversions)` — explicit labels;
342
+ - `fit_recommended(paths, conversions, anchor_timestamps)` — data-driven
343
+ boundaries from `recommend_epochs`, which segments a global daily
344
+ conversion-rate signal with a penalized piecewise-affine detector
345
+ (weekly/multi-seasonal STL decomposition, robust second-difference scale
346
+ with serial-correlation correction, PELT with a custom weighted affine
347
+ cost), enforces minimum epoch width and path-count floors, and returns
348
+ auditable left-closed, right-open boundaries. A Markov-only
349
+ `signal="transition_drift"` variant segments aligned order-1 transition
350
+ matrices. `plot_epoch_recommendation` renders the fitted segment geometry,
351
+ kept and rejected knots, and per-epoch path counts.
352
+
353
+ Sample-size diagnostics (`get_epoch_summary`,
354
+ `get_channel_epoch_sample_sizes`, order- and strategy-aware effective
355
+ floors, and the `suggest_freq` calendar sanity check) flag thin epochs
356
+ before you interpret an adjacent-epoch jump.
357
+
358
+ ### Reporting and export
359
+
360
+ Every fitted model — static, temporal, or a standalone recommendation —
361
+ renders deterministic plain text:
362
+
363
+ ```python
364
+ print(model.summary()) # compact ASCII tables
365
+ print(model.summary(output_format="report")) # natural-language narrative
366
+ ```
367
+
368
+ Summaries distill rather than dump: bounded top-k detail tables (signed
369
+ removal effects, top transitions and drop-offs, top coalitions, path
370
+ diagnostics) with the complete grids available programmatically via
371
+ `to_dataframes()` and the `get_*` accessors. The reporting language is
372
+ descriptive; it never labels a movement statistically significant or causal.
373
+
374
+ ---
375
+
376
+ ## Detailed examples
377
+
378
+ All five notebooks run end to end on the same GA4 journey extract
379
+ (`examples/ga4_attribution_journeys.csv`) — a transformed journey-level
380
+ extract created from
381
+ [Google's public obfuscated GA4 ecommerce sample](https://developers.google.com/analytics/bigquery/web-ecommerce-demo-dataset)
382
+ (Google Merchandise Store): one row per journey with an ordered channel
383
+ path, a binary conversion flag, and an anchor timestamp.
384
+
385
+ | Example | Shows |
386
+ | --- | --- |
387
+ | [`examples/basic_markov.ipynb`](https://github.com/8139CAUSAL/do_attribution/blob/main/examples/basic_markov.ipynb) | static Markov fit, shares, removal effects, summary, transition graph |
388
+ | [`examples/basic_shapley.ipynb`](https://github.com/8139CAUSAL/do_attribution/blob/main/examples/basic_shapley.ipynb) | Shapley shares, raw values, coalition results, coalition plot |
389
+ | [`examples/side_by_side.ipynb`](https://github.com/8139CAUSAL/do_attribution/blob/main/examples/side_by_side.ipynb) | `CompareAttribution` aligned tables, differences, comparison plot |
390
+ | [`examples/temporal_markov.ipynb`](https://github.com/8139CAUSAL/do_attribution/blob/main/examples/temporal_markov.ipynb) | weekly Markov epochs, diagnostics, `to_dataframes()`, epoch recommendation |
391
+ | [`examples/temporal_shapley.ipynb`](https://github.com/8139CAUSAL/do_attribution/blob/main/examples/temporal_shapley.ipynb) | weekly Shapley epochs, channel trends, diagnostics, epoch recommendation |
392
+
393
+
394
+ ---
395
+
396
+ ## Project status
397
+
398
+ `do-attribution` is in pre-1.0 development. The public API may change before
399
+ version 0.1.
400
+
401
+ ---
402
+
403
+ ## License and attribution
404
+
405
+ MIT © 2026 Aaron Goodin — see [`LICENSE`](https://github.com/8139CAUSAL/do_attribution/blob/main/LICENSE).
406
+
407
+ The bundled example dataset and the figures generated from it derive from
408
+ [Google's public obfuscated GA4 ecommerce sample](https://developers.google.com/analytics/bigquery/web-ecommerce-demo-dataset)
409
+ (Google Merchandise Store), transformed into a journey-level extract for
410
+ demonstration. Attribution outputs describe statistical association under
411
+ each model's assumptions; they are not causal effect estimates.