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.
- do_attribution-0.0.1.post1/LICENSE +21 -0
- do_attribution-0.0.1.post1/MANIFEST.in +19 -0
- do_attribution-0.0.1.post1/PKG-INFO +411 -0
- do_attribution-0.0.1.post1/README.md +371 -0
- do_attribution-0.0.1.post1/do_attribution/__init__.py +115 -0
- do_attribution-0.0.1.post1/do_attribution/base.py +147 -0
- do_attribution-0.0.1.post1/do_attribution/compare.py +138 -0
- do_attribution-0.0.1.post1/do_attribution/markov.py +502 -0
- do_attribution-0.0.1.post1/do_attribution/reporting.py +822 -0
- do_attribution-0.0.1.post1/do_attribution/shapley.py +244 -0
- do_attribution-0.0.1.post1/do_attribution/temporal.py +1546 -0
- do_attribution-0.0.1.post1/do_attribution/temporal_diagnostics.py +214 -0
- do_attribution-0.0.1.post1/do_attribution/temporal_markov.py +1509 -0
- do_attribution-0.0.1.post1/do_attribution/temporal_shapley.py +1383 -0
- do_attribution-0.0.1.post1/do_attribution/utils.py +299 -0
- do_attribution-0.0.1.post1/do_attribution/visualization.py +333 -0
- do_attribution-0.0.1.post1/do_attribution.egg-info/PKG-INFO +411 -0
- do_attribution-0.0.1.post1/do_attribution.egg-info/SOURCES.txt +53 -0
- do_attribution-0.0.1.post1/do_attribution.egg-info/dependency_links.txt +1 -0
- do_attribution-0.0.1.post1/do_attribution.egg-info/requires.txt +21 -0
- do_attribution-0.0.1.post1/do_attribution.egg-info/top_level.txt +1 -0
- do_attribution-0.0.1.post1/examples/basic_markov.ipynb +755 -0
- do_attribution-0.0.1.post1/examples/basic_shapley.ipynb +807 -0
- do_attribution-0.0.1.post1/examples/ga4_attribution_journeys.csv +272793 -0
- do_attribution-0.0.1.post1/examples/images/ga4_coalition_values.png +0 -0
- do_attribution-0.0.1.post1/examples/images/ga4_markov_attribution.png +0 -0
- do_attribution-0.0.1.post1/examples/images/ga4_markov_epoch_recommendation.png +0 -0
- do_attribution-0.0.1.post1/examples/images/ga4_markov_removal_effects.png +0 -0
- do_attribution-0.0.1.post1/examples/images/ga4_model_comparison.png +0 -0
- do_attribution-0.0.1.post1/examples/images/ga4_shapley_attribution.png +0 -0
- do_attribution-0.0.1.post1/examples/images/ga4_shapley_epoch_recommendation.png +0 -0
- do_attribution-0.0.1.post1/examples/images/ga4_temporal_markov.png +0 -0
- do_attribution-0.0.1.post1/examples/images/ga4_temporal_shapley.png +0 -0
- do_attribution-0.0.1.post1/examples/images/ga4_transition_graph.png +0 -0
- do_attribution-0.0.1.post1/examples/side_by_side.ipynb +529 -0
- do_attribution-0.0.1.post1/examples/temporal_markov.ipynb +1538 -0
- do_attribution-0.0.1.post1/examples/temporal_shapley.ipynb +1571 -0
- do_attribution-0.0.1.post1/pyproject.toml +65 -0
- do_attribution-0.0.1.post1/requirements.txt +8 -0
- do_attribution-0.0.1.post1/setup.cfg +4 -0
- do_attribution-0.0.1.post1/setup.py +58 -0
- do_attribution-0.0.1.post1/tests/__init__.py +0 -0
- do_attribution-0.0.1.post1/tests/conftest.py +78 -0
- do_attribution-0.0.1.post1/tests/test_compare.py +110 -0
- do_attribution-0.0.1.post1/tests/test_markov.py +476 -0
- do_attribution-0.0.1.post1/tests/test_shapley.py +277 -0
- do_attribution-0.0.1.post1/tests/test_temporal_diagnostics.py +214 -0
- do_attribution-0.0.1.post1/tests/test_temporal_markov.py +274 -0
- do_attribution-0.0.1.post1/tests/test_temporal_recommendation.py +325 -0
- do_attribution-0.0.1.post1/tests/test_temporal_reporting.py +195 -0
- do_attribution-0.0.1.post1/tests/test_temporal_shapley.py +249 -0
- do_attribution-0.0.1.post1/tests/test_temporal_v2_mixed_freq.py +456 -0
- do_attribution-0.0.1.post1/tests/test_to_dataframes.py +434 -0
- do_attribution-0.0.1.post1/tests/test_utils.py +236 -0
- 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
|
+

|
|
47
|
+

|
|
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.
|