-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmain.py
More file actions
165 lines (132 loc) · 8.15 KB
/
Copy pathmain.py
File metadata and controls
165 lines (132 loc) · 8.15 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
"""
main.py
-------
End-to-end Multi-Touch Attribution (MTA) pipeline.
Usage
-----
python main.py (run from the mta_model/ folder)
Steps
-----
1. Generate 50,000 synthetic user journey events across 6 channels.
2. Run 5 rule-based attribution models (Last Touch, First Touch,
Linear, Time Decay, Position-Based).
3. Run Markov Chain data-driven attribution (removal effect method).
4. Run Shapley Value attribution (game theory method).
5. Reconcile MTA outputs against simulated MMM channel attribution.
6. Generate all visualizations and executive summary.
Note on runtime
---------------
The Shapley model evaluates 2^n channel subsets (64 for 6 channels)
across all user journeys. With 50,000 users this may take 2-3 minutes.
The Markov model uses matrix exponentiation and is very fast.
"""
import os
import sys
from src.data_prep import run_prep_pipeline
from src.rule_based import run_rule_based_models
from src.markov_chain import run_markov_pipeline
from src.shapley import run_shapley_pipeline
from src.mmm_reconciliation import run_reconciliation_pipeline
from src.visualizations import generate_all_plots
# ── Config ────────────────────────────────────────────────────────────────────
OUTPUT_DIR = os.path.join(os.path.dirname(__file__), "outputs")
SUMMARY_PATH = os.path.join(OUTPUT_DIR, "mta_executive_summary.txt")
# ── Pipeline ──────────────────────────────────────────────────────────────────
def main():
print("=" * 65)
print(" MULTI-TOUCH ATTRIBUTION (MTA) MODEL")
print(" Rule-Based + Markov Chain + Shapley Values + MMM Reconciliation")
print("=" * 65)
# ── Step 1: Data Generation ────────────────────────────────────────────
print("\n[1/6] Generating User Journey Data")
journeys, converting, channel_summary = run_prep_pipeline()
# ── Step 2: Rule-Based Models ──────────────────────────────────────────
print("\n[2/6] Rule-Based Attribution Models")
rule_results = run_rule_based_models(journeys)
# ── Step 3: Markov Chain ───────────────────────────────────────────────
print("\n[3/6] Markov Chain Attribution")
markov_results, trans_matrix_df, base_rate = run_markov_pipeline(journeys)
# ── Step 4: Shapley Values ─────────────────────────────────────────────
print("\n[4/6] Shapley Value Attribution")
print(" Note: evaluating 64 channel subsets — may take 1-2 minutes...")
shapley_results = run_shapley_pipeline(journeys)
# ── Step 5: MMM Reconciliation ─────────────────────────────────────────
print("\n[5/6] MMM Reconciliation")
mmm_results, recon = run_reconciliation_pipeline(
journeys, markov_results, shapley_results, channel_summary
)
# ── Step 6: Visualizations ─────────────────────────────────────────────
print("\n[6/6] Generating Visualizations")
generate_all_plots(
journeys, rule_results, markov_results,
shapley_results, trans_matrix_df, recon, channel_summary
)
# ── Executive Summary ──────────────────────────────────────────────────
os.makedirs(OUTPUT_DIR, exist_ok=True)
summary_text = build_executive_summary(
journeys, channel_summary, markov_results, shapley_results, recon
)
with open(SUMMARY_PATH, "w", encoding="utf-8") as f:
f.write(summary_text)
print(f" Executive summary saved to: {SUMMARY_PATH}")
# Save key outputs
rule_results.to_csv(os.path.join(OUTPUT_DIR, "rule_based_results.csv"), index=False)
markov_results.to_csv(os.path.join(OUTPUT_DIR, "markov_results.csv"), index=False)
shapley_results.to_csv(os.path.join(OUTPUT_DIR, "shapley_results.csv"), index=False)
recon.to_csv(os.path.join(OUTPUT_DIR, "mta_mmm_reconciliation.csv"), index=False)
print("\n" + "=" * 65)
print(" PIPELINE COMPLETE")
print(f" Outputs saved to: {OUTPUT_DIR}")
print("=" * 65)
# ── Executive Summary ─────────────────────────────────────────────────────────
def build_executive_summary(journeys, channel_summary,
markov_results, shapley_results, recon) -> str:
total_users = journeys["user_id"].nunique()
conv_users = journeys[journeys["converted"] == 1]["user_id"].nunique()
total_spend = channel_summary["total_cost"].sum()
total_rev = journeys[journeys["is_last_touch"] == 1]["order_value"].sum()
lines = []
lines.append("=" * 70)
lines.append(" MULTI-TOUCH ATTRIBUTION — EXECUTIVE SUMMARY")
lines.append(" Rule-Based + Markov Chain + Shapley + MMM Reconciliation")
lines.append("=" * 70)
lines.append(f"\n OVERVIEW")
lines.append(f" Total Users : {total_users:,}")
lines.append(f" Conversions : {conv_users:,} ({conv_users/total_users:.1%})")
lines.append(f" Total Ad Spend : ${total_spend:,.2f}")
lines.append(f" Total Revenue : ${total_rev:,.2f}")
lines.append(f" Overall ROAS : {total_rev/total_spend:.2f}x")
lines.append(f"\n DATA-DRIVEN ATTRIBUTION (Markov Chain)")
lines.append(f" {'Channel':<20} {'Share':>8} {'Revenue':>12} {'ROI':>8}")
lines.append(" " + "-" * 52)
for _, row in markov_results.sort_values("attributed_revenue", ascending=False).iterrows():
ch_cost = channel_summary[channel_summary["channel"] == row["channel"]]["total_cost"].values
roi = row["attributed_revenue"] / ch_cost[0] if len(ch_cost) > 0 and ch_cost[0] > 0 else 0
lines.append(f" {row['channel']:<20} {row['attribution_share']:>7.1%} "
f"${row['attributed_revenue']:>11,.2f} {roi:>7.1f}x")
lines.append(f"\n MTA vs MMM RECONCILIATION")
lines.append(f" {'Channel':<20} {'MTA Share':>10} {'MMM Share':>10} {'Signal'}")
lines.append(" " + "-" * 65)
for _, row in recon.iterrows():
lines.append(f" {row['channel']:<20} {row['mta_blended_share']:>9.1%} "
f"{row['mmm_share']:>9.1%} {row['signal_strength']}")
lines.append(f"\n KEY FINDINGS")
lines.append(f"\n 1. LAST TOUCH SIGNIFICANTLY OVERSTATES paid_search and direct")
lines.append(f" Data-driven models show these channels benefit from upstream")
lines.append(f" awareness-building that last touch ignores entirely.")
lines.append(f"\n 2. DISPLAY AND PAID SOCIAL ARE UNDERVALUED by rule-based models")
lines.append(f" Markov removal effects show these channels have high impact")
lines.append(f" as assist channels even when they rarely get last-touch credit.")
lines.append(f"\n 3. MTA vs MMM RECONCILIATION reveals measurement gaps")
high_div = recon[recon["abs_divergence"] > 0.05]
for _, row in high_div.iterrows():
lines.append(f" {row['channel']}: {row['recommendation']}")
lines.append(f"\n 4. RECOMMENDED MEASUREMENT FRAMEWORK")
lines.append(f" Use MMM for: annual budget allocation, CFO conversations")
lines.append(f" Use MTA for: weekly campaign optimization, bid strategy")
lines.append(f" Use reconciliation to: identify where signals diverge most")
lines.append(f" and investigate root cause before making budget decisions.")
lines.append("\n" + "=" * 70 + "\n")
return "\n".join(lines)
if __name__ == "__main__":
main()