From 38cf0c555c179d7e054409edbc81be228a6de5ed Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 5 Jun 2026 17:23:11 +0000 Subject: [PATCH 1/4] Add Adrian Morrison 1-month paid-trial iCAC & LTV workbook Live-formula xlsx comparing current 3-mo paid trial ($90/paid trial) to a 1-mo trial paid per full-price conversion at $250/$300/$350, plus a 12,000 paid-trial stretch volume. iCAC = payout x CVR / IAF (validated against the prior US rate-scenarios deck). LTV per FP shop is a single input cell wired to shopify-dw.marketing.shop_ltv_mart_predictions_with_forecast; LTV and LTV:CAC rows recompute once it is filled. Co-authored-by: johnkalis --- .../adrian_morrison_1mo_trial_icac_ltv.xlsx | Bin 0 -> 12816 bytes analysis/adrian-morrison/build_workbook.py | 342 ++++++++++++++++++ 2 files changed, 342 insertions(+) create mode 100644 analysis/adrian-morrison/adrian_morrison_1mo_trial_icac_ltv.xlsx create mode 100644 analysis/adrian-morrison/build_workbook.py diff --git a/analysis/adrian-morrison/adrian_morrison_1mo_trial_icac_ltv.xlsx b/analysis/adrian-morrison/adrian_morrison_1mo_trial_icac_ltv.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..f3783ea5871ddad2b25647ae2153f17bce0e9448 GIT binary patch literal 12816 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

*he{$&OO!y z7@p*m7fznv4E9&fbC~lx-UseRgU&!LDaqesh-Tl!dt$c3$98VaOv}tG> zkKw`}6+9n-6lH)xP(lCqI_p>G|NaSheH#C>>iVtq+Y;x$Y=M9RfP4RK{oe)8Z_VEp z0{>&a^7`@rp)B|dJFhg z3Hlo_&full-price conversion) TO a 1-month paid trial where + the affiliate is paid PER FULL-PRICE conversion (historic ~49% conversion). + * Volumes: 8,900 paid trials / mo (base), 12,000 paid trials / mo (stretch). + * iCAC = payout x CVR / IAF, where IAF (incrementality factor) = 0.38. + check: $207 -> $267, $225 -> $290, $250 -> $322 (matches prior deck) + * Monthly spend (new model) = payout x FP shops (pay per FP conversion). + * LTV per FP shop comes from: + shopify-dw.marketing.shop_ltv_mart_predictions_with_forecast + That value is the single yellow INPUT cell (Assumptions!B9). Every LTV / + LTV:CAC figure recomputes the moment it is filled in. + +Everything in the Scenarios sheet is LIVE FORMULAS referencing the Assumptions +sheet, so payouts, volumes, CVR, IAF and LTV can all be tweaked in Google Sheets. +""" + +from openpyxl import Workbook +from openpyxl.styles import Font, PatternFill, Alignment, Border, Side +from openpyxl.comments import Comment + +# ---------------------------------------------------------------- styling +TITLE = Font(bold=True, size=14, color="FFFFFF") +HDR = Font(bold=True, size=11, color="FFFFFF") +SUBHDR = Font(bold=True, size=11) +BOLD = Font(bold=True) +ITAL = Font(italic=True, color="666666") + +GREEN = PatternFill("solid", fgColor="2E7D32") +DARK = PatternFill("solid", fgColor="37474F") +BLUE = PatternFill("solid", fgColor="1565C0") +GREYHDR = PatternFill("solid", fgColor="ECEFF1") +INPUT = PatternFill("solid", fgColor="FFF59D") # yellow = editable input +SECTION = PatternFill("solid", fgColor="CFD8DC") + +thin = Side(style="thin", color="B0BEC5") +BORDER = Border(left=thin, right=thin, top=thin, bottom=thin) +CENTER = Alignment(horizontal="center", vertical="center", wrap_text=True) +LEFT = Alignment(horizontal="left", vertical="center", wrap_text=True) +RIGHT = Alignment(horizontal="right", vertical="center") + +CUR = '"$"#,##0' +CUR2 = '"$"#,##0.00' +PCT = '0%' +PCT1 = '0.0%' +RATIO = '0.00"x"' +NUM = '#,##0' + + +def style_cell(ws, ref, *, font=None, fill=None, align=None, fmt=None, border=True): + c = ws[ref] + if font: + c.font = font + if fill: + c.fill = fill + if align: + c.alignment = align + if fmt: + c.number_format = fmt + if border: + c.border = BORDER + return c + + +# ================================================================ ASSUMPTIONS +wb = Workbook() +a = wb.active +a.title = "Assumptions" + +a["A1"] = "Assumptions / Inputs (edit these — everything else recalculates)" +a.merge_cells("A1:C1") +style_cell(a, "A1", font=TITLE, fill=DARK, align=LEFT) +for col in ("B1", "C1"): + a[col].fill = DARK + +rows = [ + ("Paid trials / mo — base (current)", 8900, NUM, "Held constant in base scenarios."), + ("Paid trials / mo — stretch", 12000, NUM, "Stretch volume scenario."), + ("1-month trial -> Full-Price CVR", 0.49, PCT, "Historic 1-mo trial conversion."), + ("Current 3-month trial CVR", 0.31, PCT, "Historic 3-mo trial conversion (~30%)."), + ("IAF (incrementality factor)", 0.38, "0.00", "iCAC = payout x CVR / IAF."), + ("iCAC target ($)", 267, CUR, "US iCAC benchmark."), + ("Current payout ($ / paid trial)", 90, CUR, "Current 3-mo model pays per paid trial."), + ("LTV per FP shop ($)", None, CUR, + "INPUT: predicted LTV per shop from " + "shopify-dw.marketing.shop_ltv_mart_predictions_with_forecast " + "(US new shops via Adrian's funnel). Fill this cell."), + ("New payout 1 ($ / FP conversion)", 250, CUR, "Per full-price conversion."), + ("New payout 2 ($ / FP conversion)", 300, CUR, "Per full-price conversion."), + ("New payout 3 ($ / FP conversion)", 350, CUR, "Per full-price conversion."), +] +for i, (label, val, fmt, note) in enumerate(rows, start=2): + style_cell(a, f"A{i}", font=BOLD, align=LEFT, fill=GREYHDR) + a[f"A{i}"] = label + c = style_cell(a, f"B{i}", align=RIGHT, fmt=fmt) + if val is not None: + c.value = val + style_cell(a, f"C{i}", font=ITAL, align=LEFT) + a[f"C{i}"] = note + +# Highlight the LTV input cell (row 9) +ltv_cell = a["B9"] +ltv_cell.fill = INPUT +ltv_cell.font = Font(bold=True, color="B71C1C") +ltv_cell.comment = Comment( + "Paste predicted LTV per shop from\n" + "shopify-dw.marketing.shop_ltv_mart_predictions_with_forecast\n" + "(filtered to US new shops acquired via Adrian Morrison's funnel).\n" + "Until filled, all LTV / LTV:CAC rows show '-'.", + "model") + +a.column_dimensions["A"].width = 34 +a.column_dimensions["B"].width = 14 +a.column_dimensions["C"].width = 60 + +# Named references for readability in formulas +A = "Assumptions!" +TRIALS_BASE = f"{A}$B$2" +TRIALS_STR = f"{A}$B$3" +CVR_NEW = f"{A}$B$4" +CVR_CUR = f"{A}$B$5" +IAF = f"{A}$B$6" +TARGET = f"{A}$B$7" +PAY_CUR = f"{A}$B$8" +LTV = f"{A}$B$9" + +# ================================================================ SCENARIOS +s = wb.create_sheet("Scenarios") + +# columns: A label | B Current | C-E base(8900) 250/300/350 | F-H stretch(12000) 250/300/350 +cols = ["A", "B", "C", "D", "E", "F", "G", "H"] +# new-model columns -> (payout assumption cell, paid-trials cell) +new_cols = { + "C": (f"{A}$B$10", TRIALS_BASE), + "D": (f"{A}$B$11", TRIALS_BASE), + "E": (f"{A}$B$12", TRIALS_BASE), + "F": (f"{A}$B$10", TRIALS_STR), + "G": (f"{A}$B$11", TRIALS_STR), + "H": (f"{A}$B$12", TRIALS_STR), +} + +# ---- title +s["A1"] = "Adrian Morrison — 1-Month Paid-Trial Economics (iCAC & LTV by payout)" +s.merge_cells("A1:H1") +style_cell(s, "A1", font=TITLE, fill=DARK, align=LEFT) +for col in cols[1:]: + s[f"{col}1"].fill = DARK + +# ---- group header row (2) +s["A2"] = "" +style_cell(s, "A2", fill=GREYHDR) +s["B2"] = "Current (Mar–Apr)" +style_cell(s, "B2", font=HDR, fill=DARK, align=CENTER) +s.merge_cells("C2:E2") +s["C2"] = "Base volume — 8,900 paid trials / mo" +style_cell(s, "C2", font=HDR, fill=BLUE, align=CENTER) +for col in ("D", "E"): + s[f"{col}2"].fill = BLUE +s.merge_cells("F2:H2") +s["F2"] = "Stretch volume — 12,000 paid trials / mo" +style_cell(s, "F2", font=HDR, fill=GREEN, align=CENTER) +for col in ("G", "H"): + s[f"{col}2"].fill = GREEN + +# ---- payout header row (3) +s["A3"] = "Payout" +style_cell(s, "A3", font=SUBHDR, fill=GREYHDR, align=LEFT) +s["B3"] = "$90 / paid trial" +style_cell(s, "B3", font=HDR, fill=DARK, align=CENTER) +payout_hdr = {"C": "$250 / FP", "D": "$300 / FP", "E": "$350 / FP", + "F": "$250 / FP", "G": "$300 / FP", "H": "$350 / FP"} +for col, txt in payout_hdr.items(): + s[f"{col}3"] = txt + fill = BLUE if col in ("C", "D", "E") else GREEN + style_cell(s, f"{col}3", font=HDR, fill=fill, align=CENTER) + +row = 4 + + +def section(title): + global row + s[f"A{row}"] = title + s.merge_cells(f"A{row}:H{row}") + style_cell(s, f"A{row}", font=SUBHDR, fill=SECTION, align=LEFT) + for col in cols[1:]: + s[f"{col}{row}"].fill = SECTION + row += 1 + + +def metric(label, builder, fmt, *, note=None, bold=False): + """builder(col) -> formula string (without '='), or None to leave blank.""" + global row + style_cell(s, f"A{row}", font=BOLD if bold else None, align=LEFT, fill=GREYHDR) + s[f"A{row}"] = label + if note: + s[f"A{row}"].comment = Comment(note, "model") + for col in cols[1:]: + f = builder(col) + c = style_cell(s, f"{col}{row}", align=RIGHT, fmt=fmt) + if f is not None: + c.value = "=" + f + if bold: + c.font = BOLD + row += 1 + + +def rate_ref(col): + if col == "B": + return PAY_CUR + return new_cols[col][0] + + +def trials_ref(col): + if col == "B": + return TRIALS_BASE + return new_cols[col][1] + + +def cvr_ref(col): + if col == "B": + return CVR_CUR + return CVR_NEW + + +# cell helpers (current-row references) +def C(col, r): + return f"{col}{r}" + + +# ---- WHAT WE GET +section("WHAT WE GET") +r_rate = row +metric("Payout rate ($)", lambda c: rate_ref(c), CUR) +r_trials = row +metric("Paid trials / mo", lambda c: trials_ref(c), NUM) +r_cvr = row +metric("Trial -> Full-Price CVR", lambda c: cvr_ref(c), PCT) +r_fp = row +metric("Full-Price shops / mo", lambda c: f"{C(c, r_trials)}*{C(c, r_cvr)}", NUM, bold=True) +r_fpgain = row +metric("FP shops gained / mo vs current", + lambda c: f"{C(c, r_fp)}-$B${r_fp}", NUM) +metric("FP shops gained / yr vs current", + lambda c: f"({C(c, r_fp)}-$B${r_fp})*12", NUM) + +# ---- WHAT IT COSTS +section("WHAT IT COSTS") +r_spend = row +metric("Monthly spend ($)", + lambda c: (f"{C(c, r_rate)}*{C(c, r_trials)}" if c == "B" + else f"{C(c, r_rate)}*{C(c, r_fp)}"), + CUR, + note="Current pays per paid trial ($90 x trials). New model pays per " + "full-price conversion (payout x FP shops).") +metric("Annual spend ($)", lambda c: f"{C(c, r_spend)}*12", CUR) +r_cpfp = row +metric("Cost per FP shop ($)", lambda c: f"{C(c, r_spend)}/{C(c, r_fp)}", CUR) +r_icac = row +metric("iCAC ($)", + lambda c: (f"{C(c, r_rate)}/{IAF}" if c == "B" + else f"{C(c, r_rate)}*{C(c, r_cvr)}/{IAF}"), + CUR, bold=True, + note="iCAC = payout x CVR / IAF (new model). Current = $90 / IAF. " + "Reported Mar–Apr actual iCAC was ~$241.") +metric("iCAC vs $267 target", lambda c: f"{C(c, r_icac)}/{TARGET}-1", PCT, + note="Positive = over target, negative = under target.") + +# ---- LTV +section("LTV (fill Assumptions!B9 to populate)") +r_ltvshop = row +metric("LTV per FP shop ($)", lambda c: f'IF({LTV}="","-",{LTV})', CUR, + note="From shopify-dw.marketing.shop_ltv_mart_predictions_with_forecast.") +metric("Total LTV of new FP shops / mo ($)", + lambda c: f'IF({LTV}="","-",{LTV}*{C(c, r_fp)})', CUR) +metric("Total LTV of new FP shops / yr ($)", + lambda c: f'IF({LTV}="","-",{LTV}*{C(c, r_fp)}*12)', CUR) +metric("LTV : iCAC ratio", + lambda c: f'IF({LTV}="","-",{LTV}/{C(c, r_icac)})', RATIO, bold=True, + note="Lifetime value vs incremental CAC.") +metric("LTV : cost-per-FP-shop ratio", + lambda c: f'IF({LTV}="","-",{LTV}/{C(c, r_cpfp)})', RATIO) +metric("Net LTV per FP shop ($) (LTV - cost/FP shop)", + lambda c: f'IF({LTV}="","-",{LTV}-{C(c, r_cpfp)})', CUR) + +# widths +s.column_dimensions["A"].width = 38 +for col in cols[1:]: + s.column_dimensions[col].width = 16 +s.freeze_panes = "B4" + +# ================================================================ README +rd = wb.create_sheet("README") +rd["A1"] = "How this workbook works" +style_cell(rd, "A1", font=TITLE, fill=DARK, align=LEFT) +notes = [ + "", + "PURPOSE", + "Compare Adrian Morrison's affiliate economics when switching from a 3-month", + "paid trial ($90 / paid trial, ~31% conversion) to a 1-month paid trial paid", + "PER full-price conversion (~49% conversion), across payouts of $250/$300/$350.", + "", + "WHAT TO EDIT", + "Only the Assumptions tab. Everything on Scenarios is live formulas.", + "The yellow cell Assumptions!B9 (LTV per FP shop) is the one external input —", + "paste the predicted LTV per shop from:", + " shopify-dw.marketing.shop_ltv_mart_predictions_with_forecast", + "(filtered to US new shops acquired through Adrian's funnel).", + "Until it is filled, all LTV / LTV:CAC rows display '-'.", + "", + "KEY FORMULAS", + " FP shops / mo = paid trials x CVR", + " Monthly spend = payout x FP shops (new model, paid per FP)", + " = $90 x paid trials (current model)", + " Cost per FP shop = monthly spend / FP shops", + " iCAC = payout x CVR / IAF (IAF = 0.38)", + " LTV : iCAC = LTV per FP shop / iCAC", + "", + "VALIDATION (against prior US rate-scenarios deck)", + " $207 -> iCAC $267 | $225 -> iCAC $290 | $250 -> iCAC $322 (matches)", + "", + "NOTE ON 'CURRENT' COLUMN", + "Formula-derived current iCAC = $90 / 0.38 = ~$237; the deck's reported", + "Mar–Apr actual was ~$241 (minor real-world variance).", + "", + "LTV ACCESS NOTE", + "This workbook was generated in an environment without shopify-dw / BigQuery", + "credentials, so the LTV value could not be auto-queried. It is wired as an", + "input cell so it populates instantly once the value is entered.", +] +for i, line in enumerate(notes, start=2): + rd[f"A{i}"] = line + if line.isupper() and line.strip(): + rd[f"A{i}"].font = BOLD +rd.column_dimensions["A"].width = 90 + +wb.save("analysis/adrian-morrison/adrian_morrison_1mo_trial_icac_ltv.xlsx") +print("workbook written") From 8cb734f725a1f412188ea1bf97557a5bc7ac74d0 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 5 Jun 2026 17:59:50 +0000 Subject: [PATCH 2/4] Populate Adrian Morrison model with real warehouse LTV + full methodology Incorporates River's pull from shop_ltv_mart_predictions_with_forecast (US / Affiliates): 1-mo era LTV $93.35/$130.12/$164.51 and 3-mo era $74.87/$128.52/n.a. at 12/24/36mo. Rebuilds workbook with Assumptions (inputs + sources), Scenarios (current + 3 payouts x 2 volumes: iGA, spend, iCAC, iCAC vs $267, LTV@12/24/36, LTV:CAC, net contribution) and an IAF sensitivity tab (0.38/0.60/0.75). Verified: iCAC $322/$387/$451, LTV:CAC@36mo 0.66/0.55/0.47. Co-authored-by: johnkalis --- .../adrian_morrison_1mo_trial_icac_ltv.xlsx | Bin 12816 -> 12727 bytes analysis/adrian-morrison/build_workbook.py | 575 ++++++++++-------- 2 files changed, 306 insertions(+), 269 deletions(-) diff --git a/analysis/adrian-morrison/adrian_morrison_1mo_trial_icac_ltv.xlsx b/analysis/adrian-morrison/adrian_morrison_1mo_trial_icac_ltv.xlsx index f3783ea5871ddad2b25647ae2153f17bce0e9448..8fcbff805ded9d343290a67572037c6204652d30 100644 GIT binary patch delta 10287 zcmZvi1yCJJ*RD724#9)FySuv++})j^8;1bFHm(~d5ZocSySoO02Y1a)&VSDT-Sgc$ zQ#CzP^-kBb)^zuDuU-ij&F09e3Q*A40000UU>l!^+=E~nKVUFjC9rXlxJUw2>0?EX z_%)jBchWQmE2+yus!U48hS1L>|Bb2OQPphzYzvfH3Dtu_m-Z0wvaA+{Xz&=NG6NTD z!O8@?*4nSNTmz-H4Ui+A9@Vt!-E%o8Oi8YGMH4IcZk4eqw?la+XD5d#ZHi$K0cK&h_Y zl3t2`i<$Oy^x7vGtOw&1j#Yt&7 zaW_x|7%Te-pTAE;$3)S#;CK^fz{{tWXkdF83>|p`$$NJeJsffkqmI%`=`Gkg&}B>1 zB>Vg939crJ!u8|Pm{$zJc+N&&~KGT*%A5NF!?Xp`i?0^o>RxSg^ySuK$No> zWzIG50%@6d%7* zcGJC?``&4@uks$R#n1Pn4;oA8Lp|e`$-0>ZKlvb zJAM#r!YAblNn($8G+&b710G-C#63o^)ge-VH!ubE^zYFl1%k14yIG$oZ{`wc>4%76 zf^FQ1MZBN*-H!=MT?b3@ro8jKNw>-Enxj^^P&$OvvUsyztTx*G+BOnci%~ZAy_`P? z;s}S|^cyU+IZK`6%=ir&ImdS(^`^=qNQ>uOM`d}OJVwR6Xu=az94I+t%mn7PAAJ-8 z62}o)uj_u0tCJ2D$`wJs+`B%nmppW)h51#jD%wYNW%_{g3CGO!UDS=OBJ^&Z(UzArHlZ-6)XEGygHLsRkd|{NQqC?{;-dB)Ph#LpKqU02r ze)K29Q%riCu*6cga@7%YTisCmTxLzh542Q8>4uwqb3b5px-iY`^=(K!1V!eN7bL(j z<*und!`N*_^G}r&vgweYoBrfs^=%nk7Kxp)w|e=(r&4JW3h7`1BaoGot~+dItg4fR zjga9h%J9J0QUi?0_&&qEUlYMsV^-(Q!6_!USO0LJ0!`K-?jW3%^#QHs=D0M$9%z_m zO~ZX`DkjvRPIwX>C6It1v-E*w8YJC24(~-DfBlqi}}jN0H@YO+vLFv#6v1+6v~<=?TWK{A*gtw%)WxG{Nks?(z2O-?H_ z)4O*iGV?SGsdw-jZ`zS;^Bpp+0|CvmQcP@8mB@Xgx-&!%=*ssd%HPJ0P8s2SDt_hG zYtk(Xh0O`1&pB?d6RX?|=GN6>!0M8dt(2?1mrI3)8v3jL-3utPl_ZV;!6<6I=%V{0b)b#l- zP@jdL+Sfy!{B(>XY7JGjYHo={@{!Vaoy)2ZK31pi9=axmDg_m2@2i`bw)d{~Ipkc0 zM$~#mo^DPiH;+|R)(R6K;oF4lL>s5a#*M!SnD-V(#{NEH;rh zqonH}(=Sz3Wa^MNIS7in5a>K{VJ}scO=?GhaWv;h89r8U#(k-(9rAUq)E{&1VYE!K zPt#ILhiJE7H$Nz80`kFi@Ru2?8`9|M%-^XlR(h4XspF_EM;&SiB81a^*7CB^5@d=; zyms%X3-FqKfZ%3lOx;&VZ}7NbqO1e8|CGM$C-P@%Q5R%SgU}45_kjA?wPS$8vnt^l z#s7-??;nD~TU;Lk5&#e(_-FVB06sdffV7y=K?y7{z{;#Kw;kDc5yn)JE=p^X_+1A3XqEnLNfkp*)8_wz~>>28a0^cbyOSmnE1%Fk8 ze3J7h={+BRK6b&n$4cCIwz`D6IlYglK$1f&asSp38q(a$eB^WSQcRpw{Z@$m)!$)? ze8!E84meQ2%;Y#UVQ47r*!;Fo+>DAF7vmg&{WVQt4fq&~yXnqHbC47fe9eni2zzww zW+}2mhC0MRPHJ5Id1_qOjK@U~_wgY=yJ8p@OE-p6Z8#@1FO(+_gLtCj;8nh>!Ftbt zO=iL{wiEROQY4YeNIvdeZL&mu^YM0MO*C$d8?dT;KS2m3hsIw4dvCo1H)4~06l6iX zN@nYK(E&8$Q3kNEexm+TS>UoWU=}>N9P{2@g$Fdm*UzVQ81_0inCq8A$luwV2a>aq zv?Q14jGCzdHcl%E4hQ&876o*}A&QlcA%+77Q z7`QtKJgt_S*rfeYAezq_o}{a07Cqb4iwUnut?G?RMi4QGt+yy~s0g;k!BaG`z)4p$ zv3egifURu>lXKPLKk(IdBrEzn*??9~p80(KcVWkD&D@r)X}C$(u9}o=IBDR`NB1qe z^~z1%m(Trlujt`Q`8GVM=;_MoQK6t1n4WCBLf54Onm1lCysDF}*S&6!G5VPO16g&4 zLo_-A8>7-H@@}E{T!_{cMg`CIw8|B#p3(sck0Y4Ypk zsfeubxvvwiOmh>507>E-Ou35Cl0N?&AYY?Y?@NJ5SK;Jg+-obC>7E68hUJGFFyhUL zys9A^Qa-|qMiSwwZh)&_iJv#}r7~7b<;I16pL2Xlwv`QFqR|4*!vT;UQt%f<} z3Au{{cQDwTBdk7$-06!w3nW(kAm6_FKl;+idxSpIkk<_xzv*oeVMRA$87zw7JFSj{!s`E2e#Dkr? zjWT7cBykbl7}Xh@>wKnC#KdYcALeJwiBm~s5M$P!04Vye`u*^UU?|yEk~K9*=bV#* zIF^FA;4*XCW4u~a3=WxBwPFs7P`r7WT`{!GJGS~2Lz03xgz28jgjp+Kc=C?ycf<^{ zr8{CYJu^CG` zzh+vb7TO^iyfl!bSkwI&zHjT(`IhQp$gR=M9wv2s6oc?V%n?JEo%vwkt#r(sv{f`* zVT7JEzXA<_&1uN*BR#YNCghg_a%kHihW+T=#7RF<$G(vi$Y>ZL!^|hoF-EA@t|6<8TNu&ab<1# z0Xz7ZEd^QWEAoT`5LgYFS*x!+l0qz{TC&~>*;8>_jKl4)mz-o>P zUWEng`Ewcq(y^o%2EFNkf$@P4QdT`rIO4P%m_lwNkYVgV{9zjOd_|MsiZXaMPAZO3 zRd!*wM<04o=+Aq082n&ZjATM}tuY>O*`k-`yAU_?YTHNZAN~_kja(37%>|Ux6@9QQ zG-^z=5F09uO@(#n$%KANQ#}9kDUjgX zaHxWfh;t~)m>a=mRg)N+Q?jbBCLw29LC_08AitPr9JtK`YGODj<`}6U;>i_yK`i8W zs0Wpkn8co*yeyDNtjB`pDG_>Ms{1zx0bvM%E%byhn%%c}@L)bP*Bu77nUoM!J!c3( zB<62tEe!LJ5;XmdF)_@OU-r92IKuyp5lT@;_6MT}V}4jxb=L&=7YA1$zZgm!IO0DT zgRanAq9FwT!EpTxV=xh#Yw{09CSgb>oZtXleth2%(Fo$O>U97z*F7t?9uC*__Q6B) z_$;2?`%zUVSuKt&RbOf832!XqRD~U3(i--88DZn1zM{L6{yr|i)dd(XVGNL?A zfp3=^jPH)z$c@~H`Y}$$wCq+mLNAhip8r8k__tI3-2ZXPC=dA8o}J_07%6{aV6p=L zhvVIls=^5W{Q1utdc|a(3sz+)Y z-~=3I4xn7X71Lo^u0(tjx~aR{33GT^9b`rD7v3$9v=AS$R%2y3fu zxV!M+KFW)Z^pqG<>t?$ERxH!C%gZM6Me6Gdqvc)%70fm7-PDI(+w1OtjR&S70TV5a z?tGH1w@AHP`O3Mo1(pQj0$`GIv-r%$K$joLX2lgbJuTDF&5h)EAKg?rk{@^INVT8;p8Au8Kk$7#`F+ZH<+mWG zZR8x7fcUT<+pUM2NC!8)4Y?8ac{i)ZuAT(UAVmm9FQKy~l&qL09q^KUDo-UtB zWE+M3C7T2!Zt6VZ0tgcox_ufL;wI5DW?HX#X(R-MYURU|-ktVv&WG6c6Jku=#a((1 zWqmD)Pdtkj@?8HCp9Wkmetvl$zyhBjlm%m5BwWFh75D3j#qI;aS9Hq;|8~06cPV-) zpxGo7ev4T}&hQER8q{GTrK3P?uZ0-tfVM=TkFH{dFN&zFeKD`*0UO!m_DAMQO%GDe zYJ*>rlMoZ2MFz4E5>LVpVIqeE`|Hb$D2EOJui|MtEHLvFUtoX^NFPNFjmADc9p->A z1lQhv#`3>YwXEeOLF-|Ybv9zuGjlth^=3Ny@pU6%gVr$D8DL!@SH!j#*QfZ_ofOGsAtpBwx*e)s?qK6_Z`n1rtbG zugQoxp;h4*8ig=tktDQOl!ve2vfT!||_wPo(um20siz z0RXM&f5%a9fE>&p9UY2wCgOIa(F0SW2ZD7iFVL22;1jHZX5x~+PQMws9rd6zEqc|T z!FoCI?LicL8`}i&tkB4LHnXvs`w2ZjzTO9D-KH2yaje$gAfeIXTrkC93Ufn}aZAko z7z-K<^IndiZmBo>y$)BNYaHRBqw0hlNIcR>wH~BY1jdfYRx-_wL^(=fQ+p&rZVj@@ zkvQA--K9^UpG&WiL7@mg2DuIlOlRZPoMSckB~?$KT<(mQJ#PNK+9Z`50;cG=|X zm|-BY?A0izh{Hwc8+J*RvBEwzv+*cWklN^7!sxCn4V=6>j0{Szq%h!ID!QF)r~k*> zK5EN2H8mM_{Kf7RkTiQ!k<3b9bcPT^G@Vf+8c2srnIyB?74X2BEaM+RgGpT_T_XB{ zGQV|veABKzjy>z}@W)ewMSymlFtF3fEXpTa`F&|2>Yc)l;DaD6Q4es-RO0<*zyl6% zsH*7j_qS7r&h*`(HMDN+pkCzsy_UuJuD2%sT)1C>k=jeM8cPdv4$VuEUQ6w9D_Sx; zK|rb*3Pz};uWF>l)N=k@Et{A2VzXSGD`;1HC)o<#@AO#EDoe#rI+#Bx>Dn|S&Vlor z?itZ63F|{Xb2P87GkwVMsKji3CvW;7A+P=;GeTe^nr80U18!P6C)zA)k^F+uW+>lS zlC?r=+B=`v1fjKDHYLq|$5u%mZ|@qKdL76VeKW19uRXNzBf?y6a~N_2XuHwmaIyfx zS@QS&aXW0FBrRb04wZn5((^0itgeRobM}qmXdts?m!7;ou0rw84}3Ppw3ZUHW^`Fe z+H!ou#Y8WXqtT^qs(WDf{A3ilm|_QcJ*wh>Srtziw=w?m@p``Lai@v3tDarSq@5L-toO2EG#o{Ukh_<@2I4nj*n)y zSDAgK2(sM1W|mk@D6NR_c;?3KIuo{g`(%iiC=gX^9NL!_e))o1R9O23w{-U*YtoCw z83)ec)2?Tx)Tsw2*KLRgC%2XW@%{)tMel`E2o!6~bgnPh>e;e8P zk2VpaPd6!V-VSo6y<`Oplqq|2L7K*iw~{rV+gOBIgO^mekZCV%>Cv{|qS+Evio##E zqDTI#&+8mZZwbHw0OEhePXAgwdpo%MZ%0B_b z9G{@8{3=(|wEUL78w|794W>tI%~EQFE-Ie9bT?^nn%kB!Ce|bSQHz)450+=I(x)-T zP84z03Jn2|)@+CC7iaQq+mmD>3#JjKmQ$d$ifm~~fEJ(9(e#|`S6n7aIUEUo7WN5d z*I~>x^C@?i8&+anCtF6xAX8=`>jnFvKRjDSb$ZE_ z?86>>OokZcZ0N$SPRtgxI~XPmm{`(8xFALS@AH9XL*$R+=0b!GcY5gcaypbxdgCH> z1NpvIJ+hReFOfcc%bqn9^vUmz%=pi;ufD!SbTJK>rg2Pk(&VGYqZX%!7C9NjBf`Fd z7q0b#o-bp=gk~|@`IP5nNL;_=x@sZxW6$STx_T0r$vi|>Xe;0mp%g6S2Xf7` zsCfXFz%6YZooz%YgkGoG3YGd4>53nFB_@KT$g$|+vB7NO=nC(xT*!)kAbM8p8_00v zDw&c?A(B1hXj=Hf`V5;-^YRAYEq0nO-R8iWbRg?k>?y1?+SU9c;uOYYqfOj~p!I!N{D z+@x#acbk))yAE00#Z(ftVr#cvM0T8qAQ`AOjU6DmTq3(Am&s}+>uINzpx?rDht+N? zLLd>BL#uIR{jXP;s3}y<_tFm2;=SsM!ZnK+qTqyJw;d>${rl4`Af$w&Amr4Qcpp$e zwIBhb-UQDbg@by<4;};^HpqT>VMInKWr~S|uxbf1FevrXvF1oGjIzxCVFpnbhL59O zr&zc;Otu+poH73E7u2S@hQKh7O~?x`?`H6IRy$giwV&bIdDd#M?PS8m47^(3J8!slyQv4pBRSS?YSi;=TB>HDk1Yc@bZvz>vR}-?ULT3?)0! z?04Pq3;t*|=(W#5_U!J)mK@C`9QQcF&pr=G*+xdj>$z}BB}a>2_p5a6a=s~7i%v*Z z{nSjDgkumi^YHig-e_>0&^seo)oW(^MW^!4b3)tEVMdC{f~e>HYm}vQUrzrJ4tUeieX#DH1iGuhVIBFbC-E?)OGTxQP50w~9tfiem)x~+2=AW96*(7o zug5R8*C{r~d=Rftr$hk5?^}Vt%*XBpAGUxKT2AGD?Us!{PwxzDL}-aNwg&v zAiZtasj98i&s)Wf=xeP+7`f>a$L&0P`~udovwXQseF!n%tx)F)dhN7^w@%`gurW85 zxn>_mvT+}=p~kn$Da@x=ov^iuz~adBkd@1gtR*Bn$rRz;{P4qm<;@J&9ZX_l_f9O# z>ZmMmxP3RoC06D$w3grvoOTu%j89Y>?W_MtghMB#W`k$Wbippx8ilZ=A{CPBe3s6( z!L}=lo$+?SUa>N~mhib-^19ms{**KgugxD5ZJW=?n^4GPtNG>qz*5z@PL;L%YgIV& zBSb1hTC3Wc#*7&FT6IvV>1-)%&l!&)M9xbdT>A5UaBfXh&>j3C&}m70mGt9g?g9lk z11HJ26Yxa&&7oiKy7-qMxqk-pqQzk~SzjUOyRC~~AV-MGtZ6Py_5ixOWZ`8~zlSr1G^bjXF-LlrDdR(8 zI2+2qN8Kx+X$da?*kx?AILgjLLFcbZa8#bCdx3?u9L%S1;AN4q?5H}pbn30r>pE_k zIkS{}EP_UBeB8RPz{q`dK4j|R7I^MWFG&fXVGq_LimYiak^ z!-$84h}cLCGecU8OFOANSQ7DxmLlI@Ws1^VD`Mg9nbIZ+`26AK=KV{;;eE{JX-O$` zSBWA}VzdZ5hXs>#N~-IukjiY961Q2XL<0!X%&L|19i!F<;jZ%|CmazBZr?x*7HF?e zE;`$`2gR$vB3c%gT`_Zd>E+0IIL|#7!=;!HQKOEPuN%Nzj1j$Qi)?M0u1et2OB0m^ zIo@>M*<GMb$6`A4+rE42Og~H%f9+xARaT29Yz_?qIn_M!ET=&OD z9h-`8q+%_Q#gvPRhTaX7;4l36#U~b-z?u~HsYN=%in~rZj5OoET07pQa`N#MhslVDWS-# zyJ`{HhbcIVbv>ya3vA1yUmRb5AA7nCPYjEXQ_7Grtx}SAOrSb zX<{MkXvqT4Oix)k>rhH%Scxtf0G?;ZU7K#6@m&nQwM1TOb;-)f5&`QZo{cQ6-71|i z%G@UY*eUubk|NCK-B(S#5_&!ALP3*Q?4#rlVcIKC9bf51oZN+RW5|~YN-B-2YDdio zLgvDaNlK1%O7;*1xHdlM1zeDn05uBjEBwnH&pYia-flKK?%SK}mg7!Oa<2J3{X5(% z1eb-{-C@Tgv;4+>3c@;k>=Ls$J)@}99z!zjtN#iwS8F^_6D2igfenfZ4iv*3tLv^K zVy=&^WeirA4V6lTHA^|4Cpg|y=GR`STSp*lE);k@>!}_@3Gj|%)}#|a0xII#T~zs9 z;Fd>iHuh-2D6ePtU*gtWS$nj=x#?$>HJu;ToD@Hkmwuy12j7yUBW!oxB5|TzzDBo*23GOq5rO9l|^)LliB2 zixle+!c!(A$U`A~vb2vSGWOJ*y>tuq%DkgLX0rDjYsx3LT@V(rimUtAf>b9SDL!t4;p$RWv& zk-~%R5QOHJez_Vj3p0~RVzix-<1V?mGUYGPUybn`Bh6erAmP^?=xTdTj|A`f$O6EfqHsCb zoLU6{=p7_+l$kolbZcy~wsDmxK{On(1oD{^UOY9_Z;6lMxQUiN?|(x<_SLCkFfL<} zW5WW(q*AH4XA55y*G}D6zyGqL{`Id>4wOZEfbu6#LokV|-Ub2yxCh14;R2J^oO*vJ zuoxGQLVeAS;FL_94nnb|>FE!!F|5%6Wu}VvF5JG&$^s6T z@e{Smc1%{8TZ8ysH*--aje^#veO2=pJXhDTa2xPSj~*Nl+cXF2`a%1b)jo4$mzlZT~~hq0!Qi>15apY`j6=9qFHEBZ?c?Gn8!T@;j^FGSTelnBryNy&VH z#iyC-)sGwXGc-Mu7X3^Nlb^Nqpg_`rvjAFpgS0;5Y{ak#e6S^ywAq=UPSrd!o}522 z!*|QpR0j0=qZ^~-Nlcy5vRzr+Ynrk>X^pG(D=vEL7|x(UPM@E_IogTx=Z*AKu7(bd zw-Qv(>cg!ZW;JT>X^>^HTvdVOsexL{Je^Kt&{|@3$-zcNrs+0~qN}#uFT6t#X;oEP z2q_Fa5&CX75EkJm0Q{W$A`sSWzeGe}2ficeERmMQAv39(2h>1hD}^WhN*2e-p5Ne}KP_ zpy2l)6=oXJKbrsErT>qH2K9H!7S8YOdW^q>|BgcbBLe_}g01lX61KDckMO@k7yn3u zg3XEl2wDFMT>RUb0fYa{xBvi@e_r`r20_e&PFRWl7>B?{B+yL<02sSjI=Hhi|GEBm zE&n@|K>`41|ElE+J;;QO_%BQT>1wsVW2yxI5(zVca@c4U{`lp;+wMQQAP8Qj|8GlH XRe<@O{RRLa|31-w*PO`w=js0d_QD7U delta 10373 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 WA)1vK4FiM$f;@F$m^1x)`u_lN=B#c2 diff --git a/analysis/adrian-morrison/build_workbook.py b/analysis/adrian-morrison/build_workbook.py index dda4507c1..098f3ecb8 100644 --- a/analysis/adrian-morrison/build_workbook.py +++ b/analysis/adrian-morrison/build_workbook.py @@ -1,42 +1,55 @@ """ -Generates the Adrian Morrison 1-month paid-trial economics workbook. - -Model context (reverse-engineered from the existing "US rate scenarios" analysis -and confirmed with the requester): - - * Affiliate is moving FROM a 3-month paid trial paid at $90 / paid trial - (historic ~31% trial->full-price conversion) TO a 1-month paid trial where - the affiliate is paid PER FULL-PRICE conversion (historic ~49% conversion). - * Volumes: 8,900 paid trials / mo (base), 12,000 paid trials / mo (stretch). - * iCAC = payout x CVR / IAF, where IAF (incrementality factor) = 0.38. - check: $207 -> $267, $225 -> $290, $250 -> $322 (matches prior deck) - * Monthly spend (new model) = payout x FP shops (pay per FP conversion). - * LTV per FP shop comes from: - shopify-dw.marketing.shop_ltv_mart_predictions_with_forecast - That value is the single yellow INPUT cell (Assumptions!B9). Every LTV / - LTV:CAC figure recomputes the moment it is filled in. - -Everything in the Scenarios sheet is LIVE FORMULAS referencing the Assumptions -sheet, so payouts, volumes, CVR, IAF and LTV can all be tweaked in Google Sheets. +Adrian Morrison 1-month paid-trial economics workbook. + +Switch FROM a 3-month paid trial ($90 / paid trial, ~31% trial->FP conversion) +TO a 1-month paid trial paid PER full-price (FP) conversion (~49% conversion), +across payouts of $250 / $300 / $350, at base (8,900) and stretch (12,000) +paid-trial volumes. + +Methodology (confirmed via the Slack threads + River's pull from the warehouse): + iGA (incremental gross adds) = paid trials x IAF (IAF = 0.38) + FP shops / mo = paid trials x CVR (CVR = 0.49 for 1-mo) + Monthly spend (new model) = payout x FP shops (pay per FP conversion) + Monthly spend (current model) = $90 x paid trials (pay per paid trial) + iCAC = monthly spend / iGA ( = payout x CVR / IAF ) + check: $250 x 0.49 / 0.38 = $322 ; $300 -> $387 ; $350 -> $451 vs $267 target + Cost per FP shop = monthly spend / FP shops ( = payout, new model ) + LTV:CAC = LTV per shop / cost per FP shop + for the new model cost/FP = payout, so LTV:CAC = LTV / payout (CVR & IAF cancel) + +LTV per FP shop -- source: + shopify-dw.marketing.shop_ltv_mart_predictions_with_forecast + US, payback_channel_group = 'Affiliates', + per-shop = predicted_cumulative_total_profit_with_forecast / gross_adds_count_with_forecast + 1-month-trial era (Apr-Nov 2024 cohorts): $93.35 @12mo · $130.12 @24mo · $164.51 @36mo + 3-month-trial era (Feb-Dec 2025 cohorts): $74.87 @12mo · $128.52 @24mo · n.a. @36mo + +Defaults follow River's bolded recommendations: 1-month era = go-forward LTV, +36-month = headline horizon, all-US, IAF held at 0.38 (+ sensitivity tab). """ from openpyxl import Workbook from openpyxl.styles import Font, PatternFill, Alignment, Border, Side from openpyxl.comments import Comment +from openpyxl.utils import get_column_letter # ---------------------------------------------------------------- styling TITLE = Font(bold=True, size=14, color="FFFFFF") HDR = Font(bold=True, size=11, color="FFFFFF") SUBHDR = Font(bold=True, size=11) BOLD = Font(bold=True) -ITAL = Font(italic=True, color="666666") +ITAL = Font(italic=True, color="666666", size=9) -GREEN = PatternFill("solid", fgColor="2E7D32") -DARK = PatternFill("solid", fgColor="37474F") +DARK = PatternFill("solid", fgColor="263238") BLUE = PatternFill("solid", fgColor="1565C0") +GREEN = PatternFill("solid", fgColor="2E7D32") +TEAL = PatternFill("solid", fgColor="00695C") +AMBER = PatternFill("solid", fgColor="EF6C00") GREYHDR = PatternFill("solid", fgColor="ECEFF1") -INPUT = PatternFill("solid", fgColor="FFF59D") # yellow = editable input SECTION = PatternFill("solid", fgColor="CFD8DC") +ROWCUR = PatternFill("solid", fgColor="F5F5F5") +ROWNEW = PatternFill("solid", fgColor="FFFFFF") +INPUT = PatternFill("solid", fgColor="FFF59D") thin = Side(style="thin", color="B0BEC5") BORDER = Border(left=thin, right=thin, top=thin, bottom=thin) @@ -47,13 +60,16 @@ CUR = '"$"#,##0' CUR2 = '"$"#,##0.00' PCT = '0%' -PCT1 = '0.0%' +PCTSIGN = '+0%;\\-0%;0%' RATIO = '0.00"x"' NUM = '#,##0' +NETCUR = '"$"#,##0;[Red]-"$"#,##0' -def style_cell(ws, ref, *, font=None, fill=None, align=None, fmt=None, border=True): +def sc(ws, ref, *, font=None, fill=None, align=None, fmt=None, border=True, value=None): c = ws[ref] + if value is not None: + c.value = value if font: c.font = font if fill: @@ -67,276 +83,297 @@ def style_cell(ws, ref, *, font=None, fill=None, align=None, fmt=None, border=Tr return c -# ================================================================ ASSUMPTIONS wb = Workbook() + +# ================================================================ ASSUMPTIONS a = wb.active a.title = "Assumptions" +A = "Assumptions!" -a["A1"] = "Assumptions / Inputs (edit these — everything else recalculates)" +a["A1"] = "Assumptions / Inputs — edit these, every other tab recalculates" a.merge_cells("A1:C1") -style_cell(a, "A1", font=TITLE, fill=DARK, align=LEFT) -for col in ("B1", "C1"): - a[col].fill = DARK - -rows = [ - ("Paid trials / mo — base (current)", 8900, NUM, "Held constant in base scenarios."), - ("Paid trials / mo — stretch", 12000, NUM, "Stretch volume scenario."), - ("1-month trial -> Full-Price CVR", 0.49, PCT, "Historic 1-mo trial conversion."), - ("Current 3-month trial CVR", 0.31, PCT, "Historic 3-mo trial conversion (~30%)."), - ("IAF (incrementality factor)", 0.38, "0.00", "iCAC = payout x CVR / IAF."), - ("iCAC target ($)", 267, CUR, "US iCAC benchmark."), - ("Current payout ($ / paid trial)", 90, CUR, "Current 3-mo model pays per paid trial."), - ("LTV per FP shop ($)", None, CUR, - "INPUT: predicted LTV per shop from " - "shopify-dw.marketing.shop_ltv_mart_predictions_with_forecast " - "(US new shops via Adrian's funnel). Fill this cell."), - ("New payout 1 ($ / FP conversion)", 250, CUR, "Per full-price conversion."), - ("New payout 2 ($ / FP conversion)", 300, CUR, "Per full-price conversion."), - ("New payout 3 ($ / FP conversion)", 350, CUR, "Per full-price conversion."), -] -for i, (label, val, fmt, note) in enumerate(rows, start=2): - style_cell(a, f"A{i}", font=BOLD, align=LEFT, fill=GREYHDR) - a[f"A{i}"] = label - c = style_cell(a, f"B{i}", align=RIGHT, fmt=fmt) +sc(a, "A1", font=TITLE, fill=DARK, align=LEFT) +sc(a, "B1", fill=DARK); sc(a, "C1", fill=DARK) +sc(a, "A2", value="Input", font=HDR, fill=DARK, align=LEFT) +sc(a, "B2", value="Value", font=HDR, fill=DARK, align=CENTER) +sc(a, "C2", value="Source / note", font=HDR, fill=DARK, align=LEFT) + +# (row, label, value, fmt, source) -- row index in sheet +arows = { + 3: ("Paid trials / mo — base (current)", 8900, NUM, "Mar–Apr actuals; held constant in base scenarios."), + 4: ("Paid trials / mo — stretch", 12000, NUM, "Stretch volume scenario."), + 5: ("1-month trial → Full-Price CVR", 0.49, PCT, "Historic 1-mo trial conversion (vs ~31% at 3-mo)."), + 6: ("Current 3-month trial CVR", 0.31, PCT, "Historic 3-mo trial conversion (~30%)."), + 7: ("IAF — incrementality factor (current)", 0.38, "0.00", "iGA = paid trials × IAF; iCAC = payout × CVR / IAF."), + 8: ("iCAC target (US, $)", 267, CUR, "US iCAC benchmark."), + 9: ("Current payout ($ / paid trial)", 90, CUR, "Current 3-mo model pays per paid trial."), + 10: ("Payout 1 ($ / FP conversion)", 250, CUR, "New 1-mo model pays per full-price conversion."), + 11: ("Payout 2 ($ / FP conversion)", 300, CUR, "New 1-mo model pays per full-price conversion."), + 12: ("Payout 3 ($ / FP conversion)", 350, CUR, "New 1-mo model pays per full-price conversion."), + 13: ("LTV/shop — 1-mo era @12mo ($)", 93.35, CUR2, "shop_ltv_mart_predictions_with_forecast · US · Affiliates · Apr–Nov 2024 cohorts."), + 14: ("LTV/shop — 1-mo era @24mo ($)", 130.12, CUR2, "Same source. Go-forward LTV (proposal restores 1-mo trial)."), + 15: ("LTV/shop — 1-mo era @36mo ($) [HEADLINE]", 164.51, CUR2, "Same source. Headline horizon (matches $267 iCAC basis)."), + 16: ("LTV/shop — 3-mo era @12mo ($)", 74.87, CUR2, "Same source · Feb–Dec 2025 cohorts. Current state."), + 17: ("LTV/shop — 3-mo era @24mo ($)", 128.52, CUR2, "Same source. Current state."), + 18: ("LTV/shop — 3-mo era @36mo ($)", None, CUR2, "Not yet aged to 36mo — left blank → shows 'n.a.'"), + 19: ("IAF sensitivity — low", 0.38, "0.00", "Current incrementality."), + 20: ("IAF sensitivity — mid", 0.60, "0.00", "If a fresh 1-mo funnel re-baselines incrementality."), + 21: ("IAF sensitivity — high", 0.75, "0.00", "Upper incrementality scenario."), +} +for r, (label, val, fmt, src) in arows.items(): + sc(a, f"A{r}", value=label, font=BOLD, align=LEFT, fill=GREYHDR) + cell = sc(a, f"B{r}", align=RIGHT, fmt=fmt) if val is not None: - c.value = val - style_cell(a, f"C{i}", font=ITAL, align=LEFT) - a[f"C{i}"] = note - -# Highlight the LTV input cell (row 9) -ltv_cell = a["B9"] -ltv_cell.fill = INPUT -ltv_cell.font = Font(bold=True, color="B71C1C") -ltv_cell.comment = Comment( - "Paste predicted LTV per shop from\n" - "shopify-dw.marketing.shop_ltv_mart_predictions_with_forecast\n" - "(filtered to US new shops acquired via Adrian Morrison's funnel).\n" - "Until filled, all LTV / LTV:CAC rows show '-'.", - "model") - -a.column_dimensions["A"].width = 34 -a.column_dimensions["B"].width = 14 -a.column_dimensions["C"].width = 60 - -# Named references for readability in formulas -A = "Assumptions!" -TRIALS_BASE = f"{A}$B$2" -TRIALS_STR = f"{A}$B$3" -CVR_NEW = f"{A}$B$4" -CVR_CUR = f"{A}$B$5" -IAF = f"{A}$B$6" -TARGET = f"{A}$B$7" -PAY_CUR = f"{A}$B$8" -LTV = f"{A}$B$9" + cell.value = val + sc(a, f"C{r}", value=src, font=ITAL, align=LEFT) + +a["B18"].fill = INPUT # 3-mo @36mo (deliberately blank) +a.column_dimensions["A"].width = 38 +a.column_dimensions["B"].width = 13 +a.column_dimensions["C"].width = 72 + +# absolute refs +TR_BASE, TR_STR = f"{A}$B$3", f"{A}$B$4" +CVR_NEW, CVR_CUR = f"{A}$B$5", f"{A}$B$6" +IAF, TARGET = f"{A}$B$7", f"{A}$B$8" +PAY_CUR = f"{A}$B$9" +PAY = {250: f"{A}$B$10", 300: f"{A}$B$11", 350: f"{A}$B$12"} +LTV_1MO = {12: f"{A}$B$13", 24: f"{A}$B$14", 36: f"{A}$B$15"} +LTV_3MO = {12: f"{A}$B$16", 24: f"{A}$B$17", 36: f"{A}$B$18"} +IAF_SENS = [f"{A}$B$19", f"{A}$B$20", f"{A}$B$21"] # ================================================================ SCENARIOS s = wb.create_sheet("Scenarios") -# columns: A label | B Current | C-E base(8900) 250/300/350 | F-H stretch(12000) 250/300/350 -cols = ["A", "B", "C", "D", "E", "F", "G", "H"] -# new-model columns -> (payout assumption cell, paid-trials cell) -new_cols = { - "C": (f"{A}$B$10", TRIALS_BASE), - "D": (f"{A}$B$11", TRIALS_BASE), - "E": (f"{A}$B$12", TRIALS_BASE), - "F": (f"{A}$B$10", TRIALS_STR), - "G": (f"{A}$B$11", TRIALS_STR), - "H": (f"{A}$B$12", TRIALS_STR), +# column layout +headers = [ + ("A", "Scenario", 22, LEFT), + ("B", "Trial era", 10, CENTER), + ("C", "Paid trials / mo", 10, CENTER), + ("D", "Payout ($)", 9, CENTER), + ("E", "Payout basis", 12, CENTER), + ("F", "Trial→FP CVR", 9, CENTER), + ("G", "FP shops / mo", 9, CENTER), + ("H", "iGA / mo", 9, CENTER), + ("I", "Monthly spend", 12, CENTER), + ("J", "Annual spend", 12, CENTER), + ("K", "Cost / FP shop", 9, CENTER), + ("L", "iCAC", 9, CENTER), + ("M", "iCAC vs $267", 9, CENTER), + ("N", "LTV/shop @12mo", 9, CENTER), + ("O", "LTV/shop @24mo", 9, CENTER), + ("P", "LTV/shop @36mo", 9, CENTER), + ("Q", "LTV:CAC @12mo", 9, CENTER), + ("R", "LTV:CAC @24mo", 9, CENTER), + ("S", "LTV:CAC @36mo", 9, CENTER), + ("T", "Total LTV/mo @36mo", 13, CENTER), + ("U", "Total LTV/yr @36mo", 13, CENTER), + ("V", "Net contrib/shop @36mo", 11, CENTER), + ("W", "Net contrib/yr @36mo", 13, CENTER), +] +last = "W" + +s["A1"] = "Adrian Morrison — 1-Month Paid-Trial Economics (iCAC · LTV · LTV:CAC by payout & volume)" +s.merge_cells(f"A1:{last}1") +sc(s, "A1", font=TITLE, fill=DARK, align=LEFT) +for col, *_ in headers[1:]: + sc(s, f"{col}1", fill=DARK) + +# group header row 2 +groups = [ + ("A", "E", "SCENARIO", SECTION, SUBHDR), + ("F", "H", "WHAT WE GET", BLUE, HDR), + ("I", "M", "WHAT IT COSTS", AMBER, HDR), + ("N", "S", "LTV PER SHOP & LTV:CAC", TEAL, HDR), + ("T", "W", "TOTALS & RETURNS", GREEN, HDR), +] +for c0, c1, txt, fill, font in groups: + s.merge_cells(f"{c0}2:{c1}2") + sc(s, f"{c0}2", value=txt, font=font, fill=fill, align=CENTER) + a0, a1 = ord(c0), ord(c1) + for o in range(a0, a1 + 1): + sc(s, f"{chr(o)}2", fill=fill) + +# column header row 3 +for col, label, width, align in headers: + sc(s, f"{col}3", value=label, font=SUBHDR, fill=GREYHDR, align=CENTER) + s.column_dimensions[col].width = width + +# scenario definitions +scenarios = [ + dict(name="Current (3-mo @ $90)", era="3-mo", trials=TR_BASE, payout=PAY_CUR, + cvr=CVR_CUR, basis="per paid trial", spend="trial", ltv=LTV_3MO, cur=True), + dict(name="$250 / FP · base 8.9k", era="1-mo", trials=TR_BASE, payout=PAY[250], + cvr=CVR_NEW, basis="per FP conv.", spend="fp", ltv=LTV_1MO, cur=False), + dict(name="$300 / FP · base 8.9k", era="1-mo", trials=TR_BASE, payout=PAY[300], + cvr=CVR_NEW, basis="per FP conv.", spend="fp", ltv=LTV_1MO, cur=False), + dict(name="$350 / FP · base 8.9k", era="1-mo", trials=TR_BASE, payout=PAY[350], + cvr=CVR_NEW, basis="per FP conv.", spend="fp", ltv=LTV_1MO, cur=False), + dict(name="$250 / FP · stretch 12k", era="1-mo", trials=TR_STR, payout=PAY[250], + cvr=CVR_NEW, basis="per FP conv.", spend="fp", ltv=LTV_1MO, cur=False), + dict(name="$300 / FP · stretch 12k", era="1-mo", trials=TR_STR, payout=PAY[300], + cvr=CVR_NEW, basis="per FP conv.", spend="fp", ltv=LTV_1MO, cur=False), + dict(name="$350 / FP · stretch 12k", era="1-mo", trials=TR_STR, payout=PAY[350], + cvr=CVR_NEW, basis="per FP conv.", spend="fp", ltv=LTV_1MO, cur=False), +] + +fmt_by_col = { + "C": NUM, "D": CUR, "F": PCT, "G": NUM, "H": NUM, "I": CUR, "J": CUR, + "K": CUR, "L": CUR, "M": PCTSIGN, "N": CUR2, "O": CUR2, "P": CUR2, + "Q": RATIO, "R": RATIO, "S": RATIO, "T": CUR, "U": CUR, "V": NETCUR, "W": NETCUR, } -# ---- title -s["A1"] = "Adrian Morrison — 1-Month Paid-Trial Economics (iCAC & LTV by payout)" -s.merge_cells("A1:H1") -style_cell(s, "A1", font=TITLE, fill=DARK, align=LEFT) -for col in cols[1:]: - s[f"{col}1"].fill = DARK - -# ---- group header row (2) -s["A2"] = "" -style_cell(s, "A2", fill=GREYHDR) -s["B2"] = "Current (Mar–Apr)" -style_cell(s, "B2", font=HDR, fill=DARK, align=CENTER) -s.merge_cells("C2:E2") -s["C2"] = "Base volume — 8,900 paid trials / mo" -style_cell(s, "C2", font=HDR, fill=BLUE, align=CENTER) -for col in ("D", "E"): - s[f"{col}2"].fill = BLUE -s.merge_cells("F2:H2") -s["F2"] = "Stretch volume — 12,000 paid trials / mo" -style_cell(s, "F2", font=HDR, fill=GREEN, align=CENTER) -for col in ("G", "H"): - s[f"{col}2"].fill = GREEN - -# ---- payout header row (3) -s["A3"] = "Payout" -style_cell(s, "A3", font=SUBHDR, fill=GREYHDR, align=LEFT) -s["B3"] = "$90 / paid trial" -style_cell(s, "B3", font=HDR, fill=DARK, align=CENTER) -payout_hdr = {"C": "$250 / FP", "D": "$300 / FP", "E": "$350 / FP", - "F": "$250 / FP", "G": "$300 / FP", "H": "$350 / FP"} -for col, txt in payout_hdr.items(): - s[f"{col}3"] = txt - fill = BLUE if col in ("C", "D", "E") else GREEN - style_cell(s, f"{col}3", font=HDR, fill=fill, align=CENTER) - -row = 4 - - -def section(title): - global row - s[f"A{row}"] = title - s.merge_cells(f"A{row}:H{row}") - style_cell(s, f"A{row}", font=SUBHDR, fill=SECTION, align=LEFT) - for col in cols[1:]: - s[f"{col}{row}"].fill = SECTION - row += 1 - - -def metric(label, builder, fmt, *, note=None, bold=False): - """builder(col) -> formula string (without '='), or None to leave blank.""" - global row - style_cell(s, f"A{row}", font=BOLD if bold else None, align=LEFT, fill=GREYHDR) - s[f"A{row}"] = label - if note: - s[f"A{row}"].comment = Comment(note, "model") - for col in cols[1:]: - f = builder(col) - c = style_cell(s, f"{col}{row}", align=RIGHT, fmt=fmt) - if f is not None: - c.value = "=" + f - if bold: - c.font = BOLD - row += 1 - - -def rate_ref(col): - if col == "B": - return PAY_CUR - return new_cols[col][0] - - -def trials_ref(col): - if col == "B": - return TRIALS_BASE - return new_cols[col][1] - - -def cvr_ref(col): - if col == "B": - return CVR_CUR - return CVR_NEW - - -# cell helpers (current-row references) -def C(col, r): - return f"{col}{r}" - - -# ---- WHAT WE GET -section("WHAT WE GET") -r_rate = row -metric("Payout rate ($)", lambda c: rate_ref(c), CUR) -r_trials = row -metric("Paid trials / mo", lambda c: trials_ref(c), NUM) -r_cvr = row -metric("Trial -> Full-Price CVR", lambda c: cvr_ref(c), PCT) -r_fp = row -metric("Full-Price shops / mo", lambda c: f"{C(c, r_trials)}*{C(c, r_cvr)}", NUM, bold=True) -r_fpgain = row -metric("FP shops gained / mo vs current", - lambda c: f"{C(c, r_fp)}-$B${r_fp}", NUM) -metric("FP shops gained / yr vs current", - lambda c: f"({C(c, r_fp)}-$B${r_fp})*12", NUM) - -# ---- WHAT IT COSTS -section("WHAT IT COSTS") -r_spend = row -metric("Monthly spend ($)", - lambda c: (f"{C(c, r_rate)}*{C(c, r_trials)}" if c == "B" - else f"{C(c, r_rate)}*{C(c, r_fp)}"), - CUR, - note="Current pays per paid trial ($90 x trials). New model pays per " - "full-price conversion (payout x FP shops).") -metric("Annual spend ($)", lambda c: f"{C(c, r_spend)}*12", CUR) -r_cpfp = row -metric("Cost per FP shop ($)", lambda c: f"{C(c, r_spend)}/{C(c, r_fp)}", CUR) -r_icac = row -metric("iCAC ($)", - lambda c: (f"{C(c, r_rate)}/{IAF}" if c == "B" - else f"{C(c, r_rate)}*{C(c, r_cvr)}/{IAF}"), - CUR, bold=True, - note="iCAC = payout x CVR / IAF (new model). Current = $90 / IAF. " - "Reported Mar–Apr actual iCAC was ~$241.") -metric("iCAC vs $267 target", lambda c: f"{C(c, r_icac)}/{TARGET}-1", PCT, - note="Positive = over target, negative = under target.") - -# ---- LTV -section("LTV (fill Assumptions!B9 to populate)") -r_ltvshop = row -metric("LTV per FP shop ($)", lambda c: f'IF({LTV}="","-",{LTV})', CUR, - note="From shopify-dw.marketing.shop_ltv_mart_predictions_with_forecast.") -metric("Total LTV of new FP shops / mo ($)", - lambda c: f'IF({LTV}="","-",{LTV}*{C(c, r_fp)})', CUR) -metric("Total LTV of new FP shops / yr ($)", - lambda c: f'IF({LTV}="","-",{LTV}*{C(c, r_fp)}*12)', CUR) -metric("LTV : iCAC ratio", - lambda c: f'IF({LTV}="","-",{LTV}/{C(c, r_icac)})', RATIO, bold=True, - note="Lifetime value vs incremental CAC.") -metric("LTV : cost-per-FP-shop ratio", - lambda c: f'IF({LTV}="","-",{LTV}/{C(c, r_cpfp)})', RATIO) -metric("Net LTV per FP shop ($) (LTV - cost/FP shop)", - lambda c: f'IF({LTV}="","-",{LTV}-{C(c, r_cpfp)})', CUR) - -# widths -s.column_dimensions["A"].width = 38 -for col in cols[1:]: - s.column_dimensions[col].width = 16 -s.freeze_panes = "B4" +r = 4 +for sd in scenarios: + fill = ROWCUR if sd["cur"] else ROWNEW + ltv36 = sd["ltv"][36] + guard36 = f'IF({ltv36}="","n.a.",' # close with ) + # text/value cells + sc(s, f"A{r}", value=sd["name"], font=BOLD, align=LEFT, fill=fill) + sc(s, f"B{r}", value=sd["era"], align=CENTER, fill=fill) + sc(s, f"E{r}", value=sd["basis"], align=CENTER, fill=fill) + # formula cells + f = { + "C": f"={sd['trials']}", + "D": f"={sd['payout']}", + "F": f"={sd['cvr']}", + "G": f"=C{r}*F{r}", + "H": f"=C{r}*{IAF}", + "I": (f"=D{r}*C{r}" if sd["spend"] == "trial" else f"=D{r}*G{r}"), + "J": f"=I{r}*12", + "K": f"=I{r}/G{r}", + "L": f"=I{r}/H{r}", + "M": f"=L{r}/{TARGET}-1", + "N": f"={sd['ltv'][12]}", + "O": f"={sd['ltv'][24]}", + "P": f"={guard36}{ltv36})", + "Q": f"=N{r}/K{r}", + "R": f"=O{r}/K{r}", + "S": f"={guard36}{ltv36}/K{r})", + "T": f"={guard36}{ltv36}*G{r})", + "U": f"={guard36}{ltv36}*G{r}*12)", + "V": f"={guard36}{ltv36}-K{r})", + "W": f"={guard36}({ltv36}-K{r})*G{r}*12)", + } + for col, formula in f.items(): + cell = sc(s, f"{col}{r}", value=formula, align=RIGHT, fmt=fmt_by_col[col], fill=fill) + if col in ("L", "S", "W"): + cell.font = BOLD + r += 1 + +# notes under the table +note_r = r + 1 +notes = [ + "HEADLINE: at 36-mo LTV (~$164.51/shop) the funnel is LTV-negative at every payout — LTV:CAC = LTV ÷ payout = 0.66 ($250) · 0.55 ($300) · 0.47 ($350); iCAC runs 21–69% over the $267 target.", + "BUT the proposal still wins on both structural axes vs the 3-mo status quo: CVR 31% → 49%, and 1-mo-era per-shop LTV is higher ($93.35 vs $74.87 @12mo).", + "iGA (incremental gross adds) = paid trials × IAF; iCAC = monthly spend ÷ iGA = payout × CVR ÷ IAF. LTV:CAC uses cost per FP shop (= payout for the new model).", + "Current state uses 3-mo-era LTV; @36mo not yet aged → shows 'n.a.'. New scenarios use 1-mo-era (go-forward) LTV. All-US.", +] +for i, n in enumerate(notes): + cell = s[f"A{note_r + i}"] + cell.value = ("• " + n) + cell.font = ITAL + s.merge_cells(f"A{note_r + i}:{last}{note_r + i}") + cell.alignment = LEFT + +s.freeze_panes = "C4" + +# ================================================================ IAF SENSITIVITY +iz = wb.create_sheet("IAF sensitivity") +iz["A1"] = "IAF sensitivity — iCAC moves with incrementality (LTV:CAC does not)" +iz.merge_cells("A1:E1") +sc(iz, "A1", font=TITLE, fill=DARK, align=LEFT) +for col in ("B", "C", "D", "E"): + sc(iz, f"{col}1", fill=DARK) + +iz["A2"] = ("LTV:CAC = LTV ÷ payout, so it is INDEPENDENT of IAF: $250→0.66 · $300→0.55 · $350→0.47 (36-mo). " + "IAF only changes iCAC and iCAC-vs-target below.") +iz.merge_cells("A2:E2") +sc(iz, "A2", font=ITAL, align=LEFT) + +iaf_labels = ["IAF 0.38 (low)", "IAF 0.60 (mid)", "IAF 0.75 (high)"] +payout_refs = [PAY[250], PAY[300], PAY[350]] +payout_lbls = ["$250 / FP", "$300 / FP", "$350 / FP"] + + +def iaf_block(title_row, metric): + sc(iz, f"A{title_row}", value=("iCAC ($)" if metric == "icac" else "iCAC vs $267 target"), + font=HDR, fill=(BLUE if metric == "icac" else AMBER), align=LEFT) + for j in range(1, 5): + sc(iz, f"{get_column_letter(j+0)}{title_row}", + fill=(BLUE if metric == "icac" else AMBER)) + hr = title_row + 1 + sc(iz, f"A{hr}", value="Payout \\ IAF", font=SUBHDR, fill=GREYHDR, align=LEFT) + for k, lab in enumerate(iaf_labels): + sc(iz, f"{get_column_letter(2+k)}{hr}", value=lab, font=SUBHDR, fill=GREYHDR, align=CENTER) + for pi, (pref, plab) in enumerate(zip(payout_refs, payout_lbls)): + rr = hr + 1 + pi + sc(iz, f"A{rr}", value=plab, font=BOLD, fill=GREYHDR, align=LEFT) + for k in range(3): + col = get_column_letter(2 + k) + icac = f"{pref}*{CVR_NEW}/{IAF_SENS[k]}" + if metric == "icac": + formula, fmt = f"={icac}", CUR + else: + formula, fmt = f"=({icac})/{TARGET}-1", PCTSIGN + sc(iz, f"{col}{rr}", value=formula, align=RIGHT, fmt=fmt) + return hr + 1 + 3 + + +end1 = iaf_block(4, "icac") +iaf_block(end1 + 2, "target") +iz.column_dimensions["A"].width = 18 +for col in ("B", "C", "D"): + iz.column_dimensions[col].width = 15 # ================================================================ README rd = wb.create_sheet("README") rd["A1"] = "How this workbook works" -style_cell(rd, "A1", font=TITLE, fill=DARK, align=LEFT) -notes = [ +sc(rd, "A1", font=TITLE, fill=DARK, align=LEFT, border=False) +lines = [ + "", "PURPOSE", + "Affiliate economics for Adrian Morrison switching from a 3-month paid trial ($90/paid trial,", + "~31% conversion) to a 1-month paid trial paid per full-price conversion (~49%), at payouts of", + "$250 / $300 / $350 and at base (8,900) and stretch (12,000) monthly paid-trial volumes.", "", - "PURPOSE", - "Compare Adrian Morrison's affiliate economics when switching from a 3-month", - "paid trial ($90 / paid trial, ~31% conversion) to a 1-month paid trial paid", - "PER full-price conversion (~49% conversion), across payouts of $250/$300/$350.", - "", - "WHAT TO EDIT", - "Only the Assumptions tab. Everything on Scenarios is live formulas.", - "The yellow cell Assumptions!B9 (LTV per FP shop) is the one external input —", - "paste the predicted LTV per shop from:", - " shopify-dw.marketing.shop_ltv_mart_predictions_with_forecast", - "(filtered to US new shops acquired through Adrian's funnel).", - "Until it is filled, all LTV / LTV:CAC rows display '-'.", + "TABS", + " Assumptions – every input + source. Edit here; all other tabs recalculate.", + " Scenarios – current state + 3 payouts × 2 volumes, with iCAC, LTV@12/24/36, LTV:CAC, net contribution.", + " IAF sensitivity – how iCAC moves at IAF 0.38 / 0.60 / 0.75 (LTV:CAC is IAF-independent).", "", "KEY FORMULAS", - " FP shops / mo = paid trials x CVR", - " Monthly spend = payout x FP shops (new model, paid per FP)", - " = $90 x paid trials (current model)", - " Cost per FP shop = monthly spend / FP shops", - " iCAC = payout x CVR / IAF (IAF = 0.38)", - " LTV : iCAC = LTV per FP shop / iCAC", + " FP shops/mo = paid trials × CVR", + " iGA/mo = paid trials × IAF (incremental gross adds)", + " Monthly spend = payout × FP shops (new model; current = $90 × paid trials)", + " Cost/FP shop = monthly spend / FP shops (= payout for the new model)", + " iCAC = monthly spend / iGA (= payout × CVR / IAF)", + " LTV:CAC = LTV per shop / cost per FP shop", + "", + "VALIDATION", + " iCAC: $250→$322 · $300→$387 · $350→$451 (vs $267 target).", + " LTV:CAC @36mo: $250→0.66 · $300→0.55 · $350→0.47. (matches River's Slack figures)", "", - "VALIDATION (against prior US rate-scenarios deck)", - " $207 -> iCAC $267 | $225 -> iCAC $290 | $250 -> iCAC $322 (matches)", + "LTV SOURCE", + " shopify-dw.marketing.shop_ltv_mart_predictions_with_forecast", + " US · payback_channel_group = 'Affiliates' ·", + " per-shop = predicted_cumulative_total_profit_with_forecast / gross_adds_count_with_forecast", + " 1-mo era (Apr–Nov 2024): $93.35 / $130.12 / $164.51 @ 12/24/36mo (go-forward)", + " 3-mo era (Feb–Dec 2025): $74.87 / $128.52 / n.a. @ 12/24/36mo (current state)", "", - "NOTE ON 'CURRENT' COLUMN", - "Formula-derived current iCAC = $90 / 0.38 = ~$237; the deck's reported", - "Mar–Apr actual was ~$241 (minor real-world variance).", + "DEFAULTS APPLIED (per River's recommendation)", + " Volume unit = paid trials (49% applied directly to trials).", + " Headline horizon = 36 months. LTV era = 1-mo (go-forward). Geo = all-US. IAF = 0.38 (+ sensitivity tab).", "", - "LTV ACCESS NOTE", - "This workbook was generated in an environment without shopify-dw / BigQuery", - "credentials, so the LTV value could not be auto-queried. It is wired as an", - "input cell so it populates instantly once the value is entered.", + "HEADLINE TAKEAWAY", + " On a straight per-shop basis the funnel is LTV-negative at all three payouts (36-mo LTV ~$165 < payout),", + " and iCAC is 21–69% over target — i.e. higher payouts buy the SAME customers for more. The structural", + " win is still real: compressing 3-mo → 1-mo lifts CVR (31%→49%) AND per-shop LTV ($75→$93 @12mo).", ] -for i, line in enumerate(notes, start=2): +for i, line in enumerate(lines, start=2): rd[f"A{i}"] = line if line.isupper() and line.strip(): rd[f"A{i}"].font = BOLD -rd.column_dimensions["A"].width = 90 +rd.column_dimensions["A"].width = 105 wb.save("analysis/adrian-morrison/adrian_morrison_1mo_trial_icac_ltv.xlsx") print("workbook written") From 33e70ae12ecbb3bfd1140736a6a0d9454b78aafe Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 5 Jun 2026 18:29:41 +0000 Subject: [PATCH 3/4] Add LTV-dilution handling for 1-mo trial (realization lever + sensitivity + breakeven) Addresses lead feedback that LTV may drop with a 1-month trial (marginal- converter dilution). Adds an adjustable 'LTV realization factor' (Assumptions B19, default 100%) applied to go-forward 1-mo-era LTV, a new LTV sensitivity tab (LTV:CAC & net contribution across 130%->60% of observed) and a breakeven block (breakeven LTV = payout; $350 needs ~2.13x observed LTV). Conclusion is robust across the full haircut range. Co-authored-by: johnkalis --- .../adrian_morrison_1mo_trial_icac_ltv.xlsx | Bin 12727 -> 15220 bytes analysis/adrian-morrison/build_workbook.py | 112 ++++++++++++++++-- 2 files changed, 100 insertions(+), 12 deletions(-) diff --git a/analysis/adrian-morrison/adrian_morrison_1mo_trial_icac_ltv.xlsx b/analysis/adrian-morrison/adrian_morrison_1mo_trial_icac_ltv.xlsx index 8fcbff805ded9d343290a67572037c6204652d30..a701e099427cb157ef384fc451f2c247bebba19d 100644 GIT binary patch delta 12643 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 delta 10213 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 Date: Fri, 5 Jun 2026 21:49:13 +0000 Subject: [PATCH 4/4] Add all-markets rate card tab (1-mo rate = March rate - 25%) Per-region rate card for Adrian Morrison (21 markets) with editable discount lever (default 25%) applied to the March rate. iCAC/LTV:CAC/net columns use US CVR/IAF/LTV as placeholders for non-US rows (flagged) pending market data. Co-authored-by: johnkalis --- .../adrian_morrison_1mo_trial_icac_ltv.xlsx | Bin 15220 -> 18374 bytes analysis/adrian-morrison/build_workbook.py | 76 ++++++++++++++++++ 2 files changed, 76 insertions(+) diff --git a/analysis/adrian-morrison/adrian_morrison_1mo_trial_icac_ltv.xlsx b/analysis/adrian-morrison/adrian_morrison_1mo_trial_icac_ltv.xlsx index a701e099427cb157ef384fc451f2c247bebba19d..e8fe71566b706a3820d7b670fcdfc4c07c135134 100644 GIT binary patch delta 8372 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 delta 5232 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 diff --git a/analysis/adrian-morrison/build_workbook.py b/analysis/adrian-morrison/build_workbook.py index 4d808adfe..32ec03b3a 100644 --- a/analysis/adrian-morrison/build_workbook.py +++ b/analysis/adrian-morrison/build_workbook.py @@ -407,6 +407,81 @@ def ltv_block(title_row, metric): for col in ("B", "C", "D", "E", "F"): lv.column_dimensions[col].width = 16 +# ================================================================ ALL MARKETS +# Adrian Morrison (affiliate 3219387) per-market rate card. +# "March rate" = the rate we would have paid in March (the New Rate column). +# Proposed 1-month-trial rate = March rate × (1 − discount), discount default 25%. +market_data = [ + ("Africa", 225, 75), ("Australia", 275, 300), ("Canada", 275, 350), + ("China", 275, 300), ("Denmark", 225, 350), ("France", 225, 350), + ("Germany", 275, 350), ("Hong Kong", 225, 300), ("India", 50, 25), + ("Ireland", 275, 300), ("Italy", 225, 300), ("Japan", 275, 300), + ("Latin America", 50, 50), ("Middle East", 225, 200), ("Netherlands", 225, 300), + ("New Zealand", 275, 300), ("Rest of Asia", 225, 300), ("Rest of Europe", 225, 350), + ("Spain", 275, 300), ("United Kingdom", 275, 350), ("United States", 275, 350), +] + +am = wb.create_sheet("All markets") +am["A1"] = "Adrian Morrison — all-market rate card (1-mo trial rate = March rate − 25%)" +am.merge_cells("A1:K1") +sc(am, "A1", font=TITLE, fill=DARK, align=LEFT) +for col in "BCDEFGHIJK": + sc(am, f"{col}1", fill=DARK) + +am["A2"] = ("Discount applied to the March rate (single lever — edit B2):") +am.merge_cells("A2:D2") +sc(am, "A2", font=BOLD, align=LEFT, fill=GREYHDR) +sc(am, "E2", value=0.25, fmt=PCT, align=CENTER, fill=INPUT, font=Font(bold=True, color="B71C1C")) +HC = "'All markets'!$E$2" + +am["A3"] = ("Rate columns (Old / March / 1-mo) are market-specific. CVR, IAF and LTV/shop are US figures used as " + "PLACEHOLDERS for non-US rows (only the US row is data-backed) — replace per market when available, then " + "iCAC / LTV:CAC / net update automatically.") +am.merge_cells("A3:K3") +sc(am, "A3", font=ITAL, align=LEFT) + +mh = [ + ("A", "Region", 16), ("B", "Old rate ($)", 10), ("C", "March rate ($)", 11), + ("D", "1-mo trial rate ($)", 12), ("E", "CVR", 8), ("F", "IAF", 8), + ("G", "iCAC ($)", 10), ("H", "LTV/shop @36mo ($)", 12), ("I", "LTV:CAC", 9), + ("J", "Net contrib/shop ($)", 12), ("K", "iCAC vs $267*", 10), +] +for col, label, width in mh: + sc(am, f"{col}5", value=label, font=SUBHDR, fill=GREYHDR, align=CENTER) + am.column_dimensions[col].width = width + +mr = 6 +for region, old, march in market_data: + us = region == "United States" + rowfill = ROWCUR if us else ROWNEW + sc(am, f"A{mr}", value=region, font=(BOLD if us else None), align=LEFT, fill=rowfill) + sc(am, f"B{mr}", value=old, fmt=CUR, align=RIGHT, fill=rowfill) + sc(am, f"C{mr}", value=march, fmt=CUR, align=RIGHT, fill=rowfill) + sc(am, f"D{mr}", value=f"=C{mr}*(1-{HC})", fmt=CUR2, align=RIGHT, fill=rowfill, font=BOLD) + # placeholder per-market assumptions (US-backed); editable + sc(am, f"E{mr}", value=0.49, fmt=PCT, align=RIGHT, fill=(rowfill if us else INPUT)) + sc(am, f"F{mr}", value=0.38, fmt="0.00", align=RIGHT, fill=(rowfill if us else INPUT)) + sc(am, f"G{mr}", value=f"=D{mr}*E{mr}/F{mr}", fmt=CUR, align=RIGHT, fill=rowfill) + sc(am, f"H{mr}", value=164.51, fmt=CUR2, align=RIGHT, fill=(rowfill if us else INPUT)) + sc(am, f"I{mr}", value=f"=H{mr}/D{mr}", fmt=RATIO, align=RIGHT, fill=rowfill, font=BOLD) + sc(am, f"J{mr}", value=f"=H{mr}-D{mr}", fmt=NETCUR, align=RIGHT, fill=rowfill) + sc(am, f"K{mr}", value=f"=G{mr}/{TARGET}-1", fmt=PCTSIGN, align=RIGHT, fill=rowfill) + mr += 1 + +note_mr = mr + 1 +for i, n in enumerate([ + "* iCAC vs $267 uses the US target as a reference only; market-specific iCAC targets differ.", + "1-mo trial rate = March rate × (1 − 25%). US: $350 → $262.50; UK/Canada/Rest of Europe → $262.50; most others ($300 March) → $225.", + "Shayna's UK/CA scale point: those markets carry the top $350 March rate, so they hold the most absolute room (→ $262.50).", +]): + cell = am[f"A{note_mr + i}"] + cell.value = "• " + n + cell.font = ITAL + am.merge_cells(f"A{note_mr + i}:K{note_mr + i}") + cell.alignment = LEFT + +am.freeze_panes = "B6" + # ================================================================ README rd = wb.create_sheet("README") rd["A1"] = "How this workbook works" @@ -422,6 +497,7 @@ def ltv_block(title_row, metric): " Scenarios – current state + 3 payouts × 2 volumes, with iCAC, LTV@12/24/36, LTV:CAC, net contribution.", " IAF sensitivity – how iCAC moves at IAF 0.38 / 0.60 / 0.75 (LTV:CAC is IAF-independent).", " LTV sensitivity – LTV:CAC & net contribution as go-forward LTV is haircut 130%→60% of observed, plus breakeven.", + " All markets – per-region rate card: 1-mo trial rate = March rate − 25%, with iCAC/LTV:CAC (US placeholders off-US).", "", "KEY FORMULAS", " FP shops/mo = paid trials × CVR",