The Type Library
Cohort LTV Analyzer
Build monthly acquisition cohorts from an order export and report repeat rate, cumulative revenue and contribution-margin LTV per customer by month since first order, CAC payback per cohort, and which cohorts are tracking below a…
TypeBrowse the technical files
--- name: cohort-ltv-analyzer description: Build monthly acquisition cohorts from an order export and report repeat rate, cumulative revenue and contribution-margin LTV per customer by month since first order, CAC payback per cohort, and which cohorts are tracking below a reference cohort at the same age. --- # Cohort LTV Analyzer Answers the question a blended LTV number cannot: *are the customers we are buying this quarter worth what the customers we bought in January were worth, at the same age?* It assigns every customer to the month of their first order, then measures each cohort at equal ages (M0, M1, M2 …) so a three-month-old cohort is never unfairly compared with an eight-month-old one. ## Before you run This skill ships scripts and sample data alongside this SKILL.md. Before running any command: 1. **Get the files.** Make sure the skill's other files (`scripts/`, `examples/` and anything else listed with this skill) are in your working folder at the same relative paths. Some environments load only SKILL.md; if yours did, fetch each file from this skill's published files and write it to the matching path. In Type, read them with the skill-file tools. Anywhere else, the Type Skills Library API lists every file with its path, content and `sha256`: GET `https://api.type.com/api/public/library/skills` and take the entry with slug `cohort-ltv-analyzer`. 2. **Check the copies are exact.** Compare each file's size in bytes, not characters (and its hash, where your tools report one), with the published version before running. A copy written out from the published file is fine once its byte size and hash match; never run a script you summarised or reconstructed from memory. 3. **Run from the skill's folder**, calling interpreters explicitly: `python3 scripts/…` and `bash examples/run.sh`. 4. **Try the sample first.** If the skill ships `examples/run.sh` and `examples/expected_output.txt`, run `bash examples/run.sh`; its output should match the expected file exactly. If it doesn't, stop and report the first differing line rather than running on real data. ## When to use this - CAC is rising and you need to know whether LTV is rising with it. - A finance or board question about payback period on paid acquisition. - After a channel shift (a discount-heavy campaign, a new marketplace, an influencer push) to see whether the new customers repeat like the old ones. - Before raising acquisition budget: which cohorts pay back, and by when. - Subscription and replenishment brands checking whether retention is decaying. ## Gathering the input Two local files, plus finance's definitions file if the business has one. The script never touches the network. **0. Finance's revenue definitions** (`finance/revenue_definitions.json`, optional). Finance owns this file; see `REVENUE_DEFINITIONS.md`. When it is in the working folder (or passed with `--definitions`), the skill takes the reporting timezone, reporting currency and FX rates file, net sales flags, refund date basis and excluded-order rules from it and prints its version at the top of the report. When it is absent, the first lines of the output say so and list the skill's own defaults; the numbers are exactly what earlier versions produced. Ask whether finance has this file before running, and never write one on finance's behalf. **1. Order export** (required). From the user's connected integrations: - **Shopify**: Orders export, or a report with one row per order. Use *net* product revenue (after discounts, before tax and shipping). Without a definitions file, exclude test and cancelled orders yourself. With one, keep them and include the `tags`, `financial_status` and `cancelled` columns so the skill excludes them under finance's rules and reports what it removed. Keep the `Created at` timestamp with its UTC offset and, if refunds should land when they were issued, a `refund_date` per refunded order. - **A data warehouse / GA4 BigQuery export / an ERP**: one row per order with the columns in `DATA_CONTRACT.md`. - **A 3PL or ERP** can supply `cogs`, `shipping_cost`, and fulfillment cost per order if the storefront export lacks them. The critical field is `customer_id` and it must be **stable across orders**. If guest checkouts create a new id per order, every customer looks single-purchase and the whole analysis is wrong — stitch on hashed email first and say so in the writeup. **2. Marketing spend by month** (optional but strongly recommended). Columns `cohort_month,marketing_spend`. Pull acquisition spend per calendar month from the ad platforms (Meta, Google Ads), Klaviyo/paid email if it is an acquisition channel, and any agency retainer allocated to acquisition. CAC per cohort is then `spend in that month / new customers acquired in that month`. If spend is unavailable, pass a single blended `--cac`. ## Running it ```bash python3 scripts/cohort_ltv.py \ --orders /path/to/orders.csv \ --spend /path/to/spend.csv \ --as-of 2026-09-01 \ --horizon 3 \ --fulfillment-cost 3.25 ``` Key arguments: - `--as-of YYYY-MM-DD` (required) is the only "today". A cohort month index is only shown once that month has fully elapsed as of this date, so partial months never masquerade as complete ones. - `--horizon N` is the cohort age at which cohorts are compared. Default 3 (a 90-day payback convention). Use 6 or 12 for considered-purchase brands. - `--cogs-pct N` is required only when the export has no `cogs` column. - `--payment-fee-pct` / `--payment-fee-fixed` default to 2.9% + 0.30, the common US card rate. Override for a different processor, or supply a `payment_fee` column and the flags are ignored. - `--restock-recovery-pct` defaults to 0: refunded goods are assumed unsellable. Set it to the real resale rate if returns go back into stock. - `--reference-cohort YYYY-MM` overrides the default reference (the oldest mature cohort). - `--tolerance-pct` (default 10) is how far below the reference a cohort must fall before it is flagged. - `--definitions PATH` points at finance's revenue definitions. Default: `finance/revenue_definitions.json` in the working folder, used if it exists. An explicit path that does not exist is an error. - `--json` emits the same analysis, including both triangles, as JSON, with `definitions` and `excludedOrders` keys first. Bad input exits 2 naming the file, row, and column. ## Reading and presenting the output First comes either **FINANCE DEFINITIONS** (file, version, owner, and how each rule was applied or why it could not be) or a **NOTE** that no definitions file was found, listing the defaults in use. Quote the version in the writeup; if it is the NOTE, say the figures are not on finance's definitions. Then five blocks, in this order (plus **Excluded orders** after the assumptions when a definitions file is in use: count and net_revenue per rule, and any rule that could not be checked because its column is missing): 1. **Assumptions** — every margin input, printed. Read it first; if the COGS or fee assumption is wrong, every number below is wrong. Put this in the writeup too, so nobody re-litigates the math later. 2. **Cohort summary** — customers, orders, AOV, repeat rate, revenue per customer, contribution margin per customer, CAC, and payback month. `obs` is the cohort's last fully elapsed month; rows marked `(immature)` have not reached the horizon and are excluded from comparison. A payback of `M0, lost M2` means the cohort reached CAC in M0 but later refunds (for example under a refund-date basis) or loss-making orders pulled it back below, and it is still below at its last observed month. 3. **Cumulative revenue per customer triangle** — the top line. Rising from left to right within a row is repeat purchasing. 4. **Cumulative contribution margin per customer triangle** — the same shape after product, shipping, fee, and refund costs. This is the number CAC has to be compared against, not revenue. 5. **Payback against CAC**, then **cohorts trending below reference**. How to present it: - Lead with payback: "January and February cohorts pay back in month 0; the June onward cohorts have not paid back and are at CAC $41–49 against $13 of contribution margin so far." - Distinguish a **mix problem** from a **retention problem**. If a recent cohort's M0 margin per customer is down, the first order got smaller or more discounted. If M0 holds but M1–M3 flatten, customers stopped coming back. The triangle tells you which; say which. - Immature cohorts are early signal, not a verdict. Say "M0 margin is down 38% versus the reference cohort" rather than "the June cohort is unprofitable" when June has only lived one month. - Convert to a decision: pause or reprice the channel that produced the weak cohort, or extend the acceptable payback window if cash allows. ## Limits - **Contribution margin, not net profit.** Overheads, salaries, software, warehousing rent, and agency fees are not modelled. A cohort that "pays back" covers its own variable costs and its acquisition cost, nothing more. - **CAC is blended per month**, spend divided by new customers acquired that month. It does not deduplicate organic from paid, does not model view-through or lagged attribution, and assigns none of the spend to repeat purchases of earlier cohorts. In a brand with heavy organic acquisition, real paid CAC is higher than this figure. - **No forecasting.** There is no curve fitting, no projected LTV, no survival model. Every cell is observed history. A cohort with no future months shown has no future months of data, not zero future value. - **Calendar months only.** Not weekly cohorts, not 30-day windows, not first-touch-channel cohorts. A customer's cohort is the month of their first order in the file — if the export does not go back far enough, genuinely old customers get mis-assigned to the first month of the file. Check the earliest order date before trusting the oldest cohort. - **One currency.** Without a definitions file, convert multi-currency exports before running. With one, rows in another currency are converted at the order date's rate from finance's rates file (the whole row, refund included: FX movement between sale and refund is not modelled). A missing rate stops the run naming the row. Spend, `--cac` and the cost flags must already be in the reporting currency. - **Refund timing follows finance.** Without a definitions file (or with `refunds.date_basis: order_date`), refunds are netted in the month of the original order. With `refund_date`, the refund and its cost recovery land in the month-since-first-order bucket of the refund date, so cumulative figures can fall in later months. `refunds.late_refund_days` is not used. - **Timezones.** Without a reporting timezone, `order_date` is read as its first 10 characters. With one, timestamps must carry a UTC offset; a timestamp without one stops the run rather than being guessed. - **Net sales flags change the revenue figures only.** Contribution margin is always net of discounts and refunds and includes shipping charged. Tax and gift-card flags cannot be applied (no such columns); the report says so. - **Excluded orders** are only as good as the `tags`, `financial_status` and `cancelled` columns. A rule whose column is missing is reported as not checked, and those orders must still be pre-filtered. An excluded order never makes its customer a customer: if a customer's only orders are excluded, they are in no cohort. - It cannot see subscriptions, exchanges, gift cards, or store credit as distinct events; they are only whatever the order export says they are.