# All-Channels Prospect (+ Customer) Segmentation — Design Spec
_2026-05-26 · workstation-lc · status: draft for review_
Problem
We have two prospect-segmentation pieces working:
- Fit — firmographic-proximity communities (look-alike matching prospects to
- Online behaviour — GA4-derived web intent for identified visitors.
We want a single all-channels segmentation that also folds in email (Eloqua), social (Meta/LinkedIn), offline/sales-readiness, and a contagion overlay — without each becoming its own silo, and surfaced as a proper Prospect tab in the dashboard.
The naive frame (fit × engagement × channel × product as multiplied categorical
axes) explodes into ~thousands of sparse, unactionable cells — "cubic." This spec
adopts the mesh-sourced resolution (lab-ovh #1412, gridiron #1416): the blow-up is
an analytics problem, not a storage one, and the fix is volume shrinkage.
Core idea — a layered stack, not four models
`
Layer 4 CONTAGION position vs converters across shared signals (overlay, later)
Layer 3 ROUTING fit × warmth × hottest channel → next action (lead-gen payoff)
Layer 2 ENGAGEMENT web + email + social → coverage-aware warmth
Layer 1 FIT firmographic proximity community → 5-SKU offer ✓ exists
Layer 0 PROFILE one record per prospect, all channel signals joined (substrate)
`
Humans see a flat 2-axis quadrant (Fit × Engagement); channel, offer and contagion are attributes on each prospect, not extra grid dimensions. Under the hood a shrinkage-resolved cube lets us slice any dimension combination (channel, product family, region, tenure) only as deep as the data supports.
Architecture
`
PG :5433 (CRM, communities, BvD) ┐
TS :5434 (GA4, email, orders) ├─► DuckDB (ATTACH via postgres_scanner)
Neo4j :7688 (contagion graph) ┘ │
├─ fact: prospect_interaction (1 row/interaction)
├─ canonical dims (normalized BEFORE cubing)
├─ materialized rollups (cron) + cheap query-time GROUP BY
└─ shrinkage layer (pure fns, downstream of rollup)
│
prospect_profile (1 row/prospect: fit, warmth, best channel,
offer, contagion flag, coverage)
│
Dashboard → Prospect tab (quadrant → list → card)
`
Components (each a separable, testable unit)
1. Dimension taxonomy + per-source normalizers (core/prospect_dims.py + config).
Canonical coarse value sets for fit, engagement, channel, product_family,
region, tenure, plus deterministic source→canonical mappers. Locked before
cubing — gridiron's #1 lesson: retrofitting normalization after the cube is the
expensive path. High-cardinality dims that can't fully resolve are modelled at the
collapsed level honestly (no faked granularity).
2. Interaction fact assembly (workers/build_prospect_interactions.py).
One row per prospect-interaction (email open/click, web session, social touch,
campaign response), normalized into canonical dims. Prospect identity bridged via
the existing CRO / contact_email / gravitee↔sso paths. Grain = interaction, not
aggregate (gridiron's analog).
3. DuckDB cube builder (workers/prospect_cube.py).
ATTACH Postgres + read Timescale (no ETL copy for warm data), build the fact view,
materialize the expensive rollups on a cron; reserve query-time GROUP BY for
cheap interactive cuts. Embedded/batch only — never exposed as an API (gridiron
gotcha c).
4. Shrinkage module (core/shrinkage.py) — pure, unit-tested functions, downstream
of the rollup:
- distribution cells:
out[k] = (count_k + K·parent_k)/(n+K), renormalize - continuous metrics: shrink the deviation
(cell_mean − base)·n/(n+K) - volume floor before a cell may surface (gridiron gotcha d)
- K is per-dimension/per-level, never global
5. Persistence estimator (workers/dim_persistence.py).
Split-half / period-over-period correlation per dimension → sets K inversely to
persistence (noisy dims shrink harder) and the parent walk-up order (collapse
the lowest-persistence dim first). Computed early — it gives both K and the backoff
order, so we never guess K.
6. Profile scorer (workers/compute_prospect_profile.py).
Assembles prospect_profile: fit community, coverage-aware warmth (only weight
channels we actually observe — an unobserved channel is not "cold"), best-next
channel, inherited 5-SKU offer, contagion flag, and a confidence field.
7. Prospect tab (dashboard/app.py + templates/_prospect_*.html).
Quadrant map (Fit × Engagement) → ranked prospect list → per-prospect card
(offer, best channel, contagion, coverage). Built with the frontend-design skill.
Data flow
1. Normalize sources into canonical dims → write prospect_interaction fact.
2. DuckDB builds + materializes rollups (cron).
3. Persistence estimator sets per-dim K + walk-up order.
4. Shrinkage resolves each requested slice to its supported depth.
5. Scorer writes prospect_profile (one row/prospect).
6. Prospect tab reads the profile + queries the cube for slices on demand.
Coverage-awareness (hard constraint)
Per-prospect channel coverage is uneven: firmographic ≈ all, email = Eloqua responders, web = bridge-identified visitors only, social ≈ thin per named company. The warmth score is weighted by observed channels + carries a confidence; missing channels are "unobserved," never silently scored as cold. (The GA4 dual-bridge lesson.)
Testing
core/shrinkage.py— pure-function unit tests (limits: n→0 ⇒ parent, n→∞ ⇒ cell;
- Persistence estimator — validate K ranking on a known dim.
- Profile scorer — coverage-weighting + offer-inheritance integration tests.
Phasing
1. Taxonomy + normalizers (lock canonical dims first). 2. Interaction fact + DuckDB cube (ATTACH, materialized rollups). 3. Shrinkage + persistence (the load-bearing 80%; pure fns + K/walk-up). 4. Profile scorer (coverage-aware warmth + attributes). 5. Prospect tab (quadrant → list → card). 6. Contagion overlay (Layer 4; OMEGA hyperbolic-contagion — consult droplet).
Open items / dependencies
- gridiron snippets (DM #1418, received) —
norm_shrunk/value_oa/MIN_Nvolume-floor /persistence→k_from_persistence→carry; seedcore/shrinkage.pyfrom these. - OMEGA contagion mapping for Layer 4 — consult droplet.
- DuckDB added to
requirements.txt(new embedded-analytics dependency). - Retention/refresh cadence for materialized rollups.
Credits
Architecture reframe: lab-ovh (mesh DM #1412). Build-level blueprint (grain, dim normalization, DuckDB rollup pattern, shrinkage mechanics + K-by-persistence): gridiron (mesh DM #1416).