Metadata-Version: 2.4
Name: do-attribution
Version: 0.0.1.post1
Summary: Unified multi-touch attribution library (Markov chain + Shapley value).
Author: Aaron Goodin
Author-email: Aaron Goodin <goodinaaron@causalscience.com>
License: MIT
Keywords: attribution,marketing,markov,shapley,multi-touch,analytics,incrementality
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Intended Audience :: Science/Research
Classifier: Topic :: Scientific/Engineering :: Information Analysis
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: numpy>=1.21.0
Requires-Dist: pandas>=1.3.0
Provides-Extra: viz
Requires-Dist: matplotlib>=3.3.0; extra == "viz"
Requires-Dist: networkx>=2.5; extra == "viz"
Provides-Extra: temporal
Requires-Dist: statsmodels>=0.14; extra == "temporal"
Requires-Dist: ruptures>=1.1; extra == "temporal"
Provides-Extra: excel
Requires-Dist: openpyxl>=3.0.0; extra == "excel"
Provides-Extra: dev
Requires-Dist: pytest>=7.0; extra == "dev"
Requires-Dist: matplotlib>=3.3.0; extra == "dev"
Requires-Dist: networkx>=2.5; extra == "dev"
Requires-Dist: openpyxl>=3.0.0; extra == "dev"
Requires-Dist: statsmodels>=0.14; extra == "dev"
Requires-Dist: ruptures>=1.1; extra == "dev"
Dynamic: author
Dynamic: license-file
Dynamic: requires-python

# do-attribution

**Multi-touch attribution for Python — closed-form Markov and exact Shapley,
with static and epoch-based workflows behind one package.**

![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)
![Python: 3.10+](https://img.shields.io/badge/python-3.10%2B-blue.svg)

You have customer journeys that look like `"Email > Social > Purchase"` and a
`0/1` conversion flag per journey. `do-attribution` turns those journeys into
per-channel shares of the observed conversions using two statistical
allocation models:

- an **absorbing-Markov-chain removal-effect** model
  (`MarkovAttribution`, orders 1+, three removal strategies), and
- a **cooperative-game Shapley-value** model (`ShapleyAttribution`, exact
  factorial weights over the observed coalition lattice).

Around the two models the package ships side-by-side model comparison
(`CompareAttribution`), explicit conversion-endpoint handling
(`conversion_labels`), plain-text reporting (`summary()`), flat DataFrame
export (`to_dataframes()`), matplotlib/networkx visualizations, and temporal
epoch analysis (`TemporalMarkovAttribution`, `TemporalShapleyAttribution`,
`recommend_epochs`) that refits the same static models independently inside
chronological epochs.

> **Scope guard:** these are probabilistic decompositions of observed
> co-occurrence between channel exposure and conversion — **not causal
> inference**. A high share means the channel is statistically associated
> with converting paths under the model's assumptions. It does not identify
> the channel that caused a conversion, and it does not replace
> incrementality tests or randomized experiments.

---

## Key capabilities

- **Deterministic, closed-form Markov attribution.** Removal effects come
  analytically from the absorbing-chain fundamental matrix `(I − Q)⁻¹` — no
  Monte Carlo, no seed, no simulation count. Identical inputs always produce
  identical outputs.
- **Three removal-effect definitions.** `removal_strategy=` `"fail"`,
  `"detour"` (signed effects), or `"redirect_to_null"` — three operational
  meanings of "remove this channel", selected explicitly instead of implied.
- **Exact Shapley values.** Per-channel values use the exact factorial weight
  `w(s, n) = s!·(n−s−1)!/n!`; raw signed values, the coalition value
  function, and normalized shares are all returned.
- **Conservation and contract guarantees.** Attribution shares sum to 1,
  attributed conversions conserve the observed total, `'fail'` removal
  effects are non-negative, and absorption probabilities stay on `[0, 1]` —
  enforced by the implementation.
- **Model reporting.** Every fitted model renders a compact ASCII `summary()`
  or a natural-language `summary(output_format="report")` — plain Python
  strings with bounded, distilled detail tables (signed removal effects, top
  transitions and drop-offs, top coalitions, path diagnostics).
- **Flat DataFrame export.** Temporal models expose
  `to_dataframes() -> dict[str, pd.DataFrame]`: flat, string-keyed,
  Parquet/SQL-ready tables for attribution, raw model values, and sample
  diagnostics — no text parsing, no `MultiIndex`, no lazy refits.
- **Visualization.** `plot_attribution`, `plot_removal_effects`,
  `plot_transition_graph`, `plot_coalition_values`,
  `plot_attribution_comparison`, and `plot_epoch_recommendation` each return
  a `matplotlib.figure.Figure`.
- **Temporal epoch analysis.** Calendar buckets (`freq=`), explicit labels
  (`fit_by_epoch`), or data-driven boundaries
  (`recommend_epochs` + `fit_recommended`) — every epoch is an independent
  static fit; complete paths are never split across epochs.
- **Reproducible outputs.** Deterministic model fits, deterministic
  renderers, and deterministic tie ordering — identical inputs always
  produce identical reports, tables, and figures.

Core runtime dependencies are `numpy` and `pandas`.

---

## Installation

```bash
pip install do-attribution
```

| Extra | Installs | Enables |
| --- | --- | --- |
| `viz` | `matplotlib`, `networkx` | all plotting helpers |
| `temporal` | `statsmodels`, `ruptures` | global-signal epoch recommendation |
| `excel` | `openpyxl` | Excel-friendly workflows |
| `dev` | all of the above + `pytest` | running the test suite |

```bash
pip install "do-attribution[viz]"
pip install "do-attribution[temporal,viz]"
```

Requires Python **3.10 or newer**.

---

## Quick start

A complete, runnable analysis — ten journeys, a fitted Markov model, shares,
and a readable report:

```python
from do_attribution import MarkovAttribution

journeys = [
    "Email > Social > Purchase",
    "Search > Email > Purchase",
    "Social > Purchase",
    "Email > Search",                       # no conversion
    "Search > Social > Email > Purchase",
    "Direct > Purchase",
    "Email > Social",                       # no conversion
    "Search > Email > Social > Purchase",
    "Social > Email > Purchase",
    "Search",                               # no conversion
]
conversions = [1, 1, 1, 0, 1, 1, 0, 1, 1, 0]

model = MarkovAttribution(
    order=1,
    removal_strategy="redirect_to_null",
    conversion_labels=["Purchase"],   # endpoint token, not a channel
    return_transition_matrix=True,
)
result = model.fit(journeys, conversions)

print({ch: round(share, 4) for ch, share in result["attribution"].items()})
print(f"P(conversion | start) = {result['total_conversion_probability']:.4f}")
print(model.summary())
```

Output (abridged — the summary continues with top transitions, drop-off
bottlenecks, and path diagnostics):

```text
{'Direct': 0.085, 'Email': 0.3912, 'Search': 0.1913, 'Social': 0.3324}
P(conversion | start) = 0.7000
Attribution Summary
Model: MarkovAttribution
Epoch source: static
Overall: 10 paths, 7 conversions, 70.00% conversion rate

Data
+-----------+-------+-------------+---------+--------------------------------------------+
| Cohort    | Paths | Conversions | Conv. % | Top channels                               |
+-----------+-------+-------------+---------+--------------------------------------------+
| All paths | 10    | 7           | 70.00%  | Email 39.12%, Social 33.24%, Search 19.13% |
+-----------+-------+-------------+---------+--------------------------------------------+

Removal effects (raw, signed) vs normalized shares
+---------+----------------+--------+
| Channel | Removal effect | Share  |
+---------+----------------+--------+
| Email   | +0.6571        | 39.12% |
| Social  | +0.5584        | 33.24% |
| Search  | +0.3214        | 19.13% |
| Direct  | +0.1429        | 8.50%  |
+---------+----------------+--------+
```

The Shapley model is a drop-in alternative on the same inputs:

```python
from do_attribution import ShapleyAttribution

shapley = ShapleyAttribution(conversion_labels=["Purchase"]).fit(journeys, conversions)
print({ch: round(share, 4) for ch, share in shapley["attribution"].items()})
# {'Direct': 0.0, 'Email': 0.375, 'Search': 0.0, 'Social': 0.625}
```

Lists, NumPy arrays, and pandas Series all work as path and conversion
inputs.

---

## Visual examples

All figures below are outputs from the GA4 public ecommerce journey example
(`examples/ga4_attribution_journeys.csv` — 272,792 journeys, 8 channels,
Nov 2020 – Jan 2021), generated by the example notebooks with the shipped
plotting helpers.

| | |
| :---: | :---: |
| <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"> |
| `plot_attribution` — Markov shares across the GA4 channels | `plot_transition_graph` — the fitted GA4 journey chain (edges ≥ 3%) |
| <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"> |
| `plot_attribution_comparison` — Markov vs Shapley on the same GA4 journeys | `plot_epoch_recommendation` — recommended epoch boundaries on the conversion-rate signal |

`plot_removal_effects` (signed bars) and `plot_coalition_values` (top Shapley
coalitions) complete the plotting surface; every helper returns a
`matplotlib.figure.Figure`. The full image bank lives in
[`examples/images/`](https://github.com/8139CAUSAL/do_attribution/tree/main/examples/images).

---

## Supported attribution methods

### Markov removal-effect attribution

`MarkovAttribution(order=k)` models journeys as an absorbing Markov chain
(`START`, channels or sliding `k`-tuples of channels, `CONVERSION`, `NULL`)
and credits each channel by its removal effect, computed in closed form.
Three operational definitions of removal are provided:

| Strategy | Behavior |
| --- | --- |
| `"fail"` | Strip the channel from every path, force conversions on paths containing it to zero, rebuild the chain, recompute `P(CONVERSION \| START)`. Effects ≥ 0. |
| `"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. |
| `"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. |

Higher orders (`order=2`, `order=3`, …) use sliding tuples as states; short
paths are left-padded with `START`. `fit` returns `attribution`,
`removal_effects`, `total_conversion_probability`, and (opt-in) the
`transition_matrix`.

### Shapley coalition attribution

`ShapleyAttribution` uses the value function *v(S) = conversions on paths
whose unique channel set is exactly S* and exact factorial weights. `fit`
returns `attribution`, raw signed `shapley_values`, and the observed
`value_function`:

```python
shapley["value_function"]
# {('Email', 'Social'): 2, ('Email', 'Search'): 1, ('Social',): 1, ...}
```

Because no journey may touch every channel, the raw values can sum to zero
by construction; normalization handles that regime deterministically (with a
warning) instead of producing unstable shares.

### Side-by-side comparison

```python
from do_attribution import CompareAttribution

comparison = CompareAttribution(conversion_labels=["Purchase"])
comparison.fit(journeys, conversions)

comparison.attribution_frame()      #           markov  shapley
                                    # Direct    0.0263    0.000
                                    # Email     0.3947    0.375
                                    # Search    0.2018    0.000
                                    # Social    0.3772    0.625
comparison.difference_frame("markov")
```

The two methods answer different statistical questions and can disagree
materially without either being defective — `difference_frame` makes the
disagreement explicit. `extra_models={...}` plugs any additional
`BaseAttribution` implementation into the same aligned tables, so
first-touch, last-touch, or any custom baseline can sit beside the two
built-in models.

### Conversion-endpoint declaration

Terminal outcome tokens (`"Purchase"`, `"Sale"`, …) are declared, not
guessed: `conversion_labels=["Purchase"]` strips them before channel
extraction, state construction, coalition enumeration, reporting, and
plotting. Labels colliding with the reserved `START`/`CONVERSION`/`NULL`
tokens raise `ValueError`; `conversion_labels=None` treats every token as a
channel.

### Temporal epoch analysis

`TemporalMarkovAttribution` and `TemporalShapleyAttribution` are
chronological orchestrators: each complete path is assigned to exactly one
epoch by a single anchor timestamp, each epoch gets an independent static
fit, and adjacent epochs share nothing — jumps at boundaries are valid
outputs of a discontinuous piecewise model, not smoothing artifacts.

```python
import pandas as pd
from do_attribution import TemporalShapleyAttribution

anchors = pd.to_datetime([
    "2025-01-05", "2025-01-12", "2025-02-08", "2025-02-17", "2025-04-03",
    "2025-04-21", "2025-07-09", "2025-07-19", "2025-10-02", "2025-10-18",
])

by_quarter = TemporalShapleyAttribution(
    freq="Q",                      # calendar buckets; per-year mappings supported
    min_samples_per_epoch=1,       # demo-sized floor
    conversion_labels=["Purchase"],
)
by_quarter.fit(journeys, anchors, conversions)

by_quarter.get_trend("Email")             # per-epoch share for one channel
tables = by_quarter.to_dataframes()       # flat DataFrames, ready for Parquet/SQL
list(tables)
# ['attribution', 'shapley_values', 'epoch_summary', 'channel_epoch_sample_sizes']
```

The Markov variant additionally exports `removal_effects`,
`conversion_probability`, and `transition_matrices` tables. Three entry
points cover epoch selection:

- `fit(paths, timestamps, conversions)` — calendar frequency buckets;
- `fit_by_epoch(paths, epoch_labels, conversions)` — explicit labels;
- `fit_recommended(paths, conversions, anchor_timestamps)` — data-driven
  boundaries from `recommend_epochs`, which segments a global daily
  conversion-rate signal with a penalized piecewise-affine detector
  (weekly/multi-seasonal STL decomposition, robust second-difference scale
  with serial-correlation correction, PELT with a custom weighted affine
  cost), enforces minimum epoch width and path-count floors, and returns
  auditable left-closed, right-open boundaries. A Markov-only
  `signal="transition_drift"` variant segments aligned order-1 transition
  matrices. `plot_epoch_recommendation` renders the fitted segment geometry,
  kept and rejected knots, and per-epoch path counts.

Sample-size diagnostics (`get_epoch_summary`,
`get_channel_epoch_sample_sizes`, order- and strategy-aware effective
floors, and the `suggest_freq` calendar sanity check) flag thin epochs
before you interpret an adjacent-epoch jump.

### Reporting and export

Every fitted model — static, temporal, or a standalone recommendation —
renders deterministic plain text:

```python
print(model.summary())                          # compact ASCII tables
print(model.summary(output_format="report"))    # natural-language narrative
```

Summaries distill rather than dump: bounded top-k detail tables (signed
removal effects, top transitions and drop-offs, top coalitions, path
diagnostics) with the complete grids available programmatically via
`to_dataframes()` and the `get_*` accessors. The reporting language is
descriptive; it never labels a movement statistically significant or causal.

---

## Detailed examples

All five notebooks run end to end on the same GA4 journey extract
(`examples/ga4_attribution_journeys.csv`) — a transformed journey-level
extract created from
[Google's public obfuscated GA4 ecommerce sample](https://developers.google.com/analytics/bigquery/web-ecommerce-demo-dataset)
(Google Merchandise Store): one row per journey with an ordered channel
path, a binary conversion flag, and an anchor timestamp.

| Example | Shows |
| --- | --- |
| [`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 |
| [`examples/basic_shapley.ipynb`](https://github.com/8139CAUSAL/do_attribution/blob/main/examples/basic_shapley.ipynb) | Shapley shares, raw values, coalition results, coalition plot |
| [`examples/side_by_side.ipynb`](https://github.com/8139CAUSAL/do_attribution/blob/main/examples/side_by_side.ipynb) | `CompareAttribution` aligned tables, differences, comparison plot |
| [`examples/temporal_markov.ipynb`](https://github.com/8139CAUSAL/do_attribution/blob/main/examples/temporal_markov.ipynb) | weekly Markov epochs, diagnostics, `to_dataframes()`, epoch recommendation |
| [`examples/temporal_shapley.ipynb`](https://github.com/8139CAUSAL/do_attribution/blob/main/examples/temporal_shapley.ipynb) | weekly Shapley epochs, channel trends, diagnostics, epoch recommendation |


---

## Project status

`do-attribution` is in pre-1.0 development. The public API may change before
version 0.1.

---

## License and attribution

MIT © 2026 Aaron Goodin — see [`LICENSE`](https://github.com/8139CAUSAL/do_attribution/blob/main/LICENSE).

The bundled example dataset and the figures generated from it derive from
[Google's public obfuscated GA4 ecommerce sample](https://developers.google.com/analytics/bigquery/web-ecommerce-demo-dataset)
(Google Merchandise Store), transformed into a journey-level extract for
demonstration. Attribution outputs describe statistical association under
each model's assumptions; they are not causal effect estimates.
