Semantic layer implementation of the quantitative PMF framework from Tribe Capital: Growth Accounting × Cohort Analysis × Distribution
Implemented as a dbt package.
This dbt package implements the quantitative framework developed by Tribe Capital. As outlined in their essay, achieving Product-Market Fit (PMF) is not a binary status but a spectrum. Just as the 3 Financial Statements tell us the financial health of a company, this framework uses activity data to objectively break down PMF into three orthogonal, non-derivable pillars:
Instead of looking at deceptive top-line growth, Growth Accounting dissects the exact atomic origins of metric velocity across 6 distinct buckets:
-
New: Volume from first-time entities in the current period.
-
Resurrected: Volume from entities who lapsed in the past but returned.
-
Expansion: Incremental volume from existing active entities spending/engaging more.
-
Retained: Baseline stable volume carried over from the previous period.
-
Contraction: Lost volume from existing active entities spending/engaging less.
-
Churned: Complete loss of an entity who was active in the previous period.
-
The Core Signal (
$Quick Ratio$ ): The package automates the calculation of the Growth Quick Ratio:$$\text{Quick Ratio} = \frac{\text{New} + \text{Resurrected} + \text{Expansion}}{\text{Churned} + \text{Contraction}}$$ A Quick Ratio
$> 1.0x$ indicates efficient, structural growth. A ratio$< 1.0x$ implies a "leaking bucket" where top-line acquisition cannot outpace product churn.
While Growth Accounting flags top-line efficiency, Cohort Analysis explains the underlying customer lifetime behavior. It groups entities by their birth period and tracks both Logo (Count) and Value (Revenue/GMV) retention as they age.
- The Core Signal: The ultimate mathematical proof of PMF is a cohort retention curve that flattens parallel to the x-axis at a non-zero asymptote. If curves continuously trend toward zero, the product lacks permanent value. Super-linear LTV curves (climbing upward) indicate powerful negative net churn.
A healthy average (ACV/AMU) can mask dangerous outliers. This pillar evaluates the risk and depth of your PMF spectrum:
- Ordinal Metrics (Revenue/GMV): Computes a Cumulative Distribution Function (CDF) to quantify concentration risk. It answers: Is 90% of your value coming from a few "whales" (consulting risk), or is it healthily distributed across the long-tail?
-
Binary Engagement Metrics (MAU/DAU): Implements the L28 Intensity Distribution (tracking the exact fraction of users active
$N$ days out of a 28-day window). This surfaces whether active users are deeply engaging with the product or barely scratching the surface.
# 1. Install dbt for your warehouse
pip install dbt-postgres # or dbt-bigquery, dbt-snowflake, dbt-duckdb …
# 2. Configure your warehouse connection
# Edit ~/.dbt/profiles.yml (see profiles.yml.example in this repo)
# 3. Point the package at your tables
# Edit dbt_project.yml → vars.metrics → source_table for each metric
# 4. Run
dbt rundbt writes the output tables to your warehouse.
pmf_dbt/
├── dbt_project.yml # metric definitions live here (vars.metrics)
├── macros/
│ ├── pmf_growth_accounting.sql # core logic — shared across all metrics
│ ├── pmf_cohort.sql
│ └── pmf_distribution.sql
└── models/ # one-liner: calls the macro
The macros contain all the logic. The model files are one line each. You never need to edit the macros unless you want to extend the framework.
Step 1 — Add the metric definition to dbt_project.yml under vars.metrics:
vars:
metrics:
revenue: # your metric name
source_table: "your_schema.payments"
entity_id: "merchant_id" # the "who"
time_column: "paid_at"
value_expr: "SUM(net_amount)"
is_binary: false # false = ordinal (has expansion/contraction)
time_grains: ["monthly", "weekly"]
row_filter: "status = 'settled'" # optionalStep 2 — Create model files (one per framework × grain):
# models/pmf/revenue__growth_accounting__monthly.sql
{{ pmf_growth_accounting('revenue', var('metrics')['revenue'], 'monthly') }}
# models/pmf/revenue__cohort__monthly.sql
{{ pmf_cohort('revenue', var('metrics')['revenue'], 'monthly') }}
# models/pmf/revenue__distribution__monthly.sql
{{ pmf_distribution('revenue', var('metrics')['revenue'], 'monthly') }}Step 3 — Run:
dbt run --select revenue__*| column | description |
|---|---|
metric_name |
e.g. gmv |
grain |
monthly / weekly |
period |
truncated date |
new |
value from new entities |
resurrected |
value from returned entities |
expansion |
incremental value from existing entities (null for binary) |
retained |
value from stable existing entities |
contraction |
lost value from existing entities (null for binary) |
churned |
value from lost entities |
total |
total this period |
gross_retention |
retained / prev_total |
net_churn |
(churned + contraction - resurrected - expansion) / prev_total |
quick_ratio |
(new + resurrected + expansion) / (churned + contraction) |
growth_rate |
(total - prev_total) / prev_total |
| column | description |
|---|---|
cohort_period |
first active period |
cohort_age |
periods since cohort birth |
logo_retention |
% of cohort still active |
revenue_retention |
period value / birth value (can exceed 1.0) |
ltv_per_entity |
cumulative value / cohort size |
Ordinal metrics: CDF — entity_value, pct_entities_at_or_below, pct_value_at_or_below
Binary metrics: L-N — intensity_value (days active), pct_of_active_entities, cumulative_pct
All tables share the same schema. Union them for cross-metric dashboards:
select * from gmv__growth_accounting__monthly
union all
select * from mau__growth_accounting__monthly
order by metric_name, periodis_binary: false (GMV, Revenue) |
is_binary: true (MAU, DAU) |
|
|---|---|---|
| expansion / contraction | ✓ computed | null |
| growth accounting | full 6-bucket | 4-bucket |
| distribution | CDF of value | L-N intensity |
| revenue_retention | can exceed 1.0 | always ≤ 1.0 |