Commit df4fad8
feat(lifecycle): early-pLTV (tenure-anchored) snapshot [LTV-Pm] (#120)
* feat(lifecycle): early-pLTV (tenure-anchored) snapshot [LTV-Pm]
Add the second observation regime (design.md §3.1 / D8): a tenure-anchored
snapshot that observes every customer at a fixed short tenure
(customer_start + early_tenure_weeks) — the genuine cold-start case for
acquisition-time value prediction (Voyantis framing).
- build_early_pltv_snapshot(population, sim, *, early_tenure_weeks=4, …) in
schemes/lifecycle/snapshots.py.
- Unify both regimes on one per-customer-cutoff core. The calendar and early
builders now feed a shared _assemble_snapshot() driven by a
customer_id -> cutoff map; the three aggregation helpers take that map
instead of a single date. Feature derivations, the mrr_change_full_period
trap, target attribution, and difficulty distortions are defined exactly
once. The calendar regime's output is unchanged — all LTV-Pl tests pass
as-is, and the lead-scoring distorted-snapshot hash is still byte-identical
(196bc45f…).
Semantics:
- Eligibility = survival to the anchor: drops onboarding churners (churned at
or before start+anchor), keeps late starters and customers who churn after
the anchor. The cohort therefore differs from the calendar regime's.
- Forward windows are fully simulated relative to each customer's OWN start
(engine D6 runs through max(obs, start+et)+fwd), so the anchor may
legitimately fall after observation_date — the builder does not require
cutoff <= obs (unlike the calendar regime).
- Coverage guards: early_tenure_weeks must be >= 1 and <= the sim's recorded
early_tenure_weeks (else per-customer forward windows would be censored),
on top of the shared forward-window / population-mismatch / observation-date
checks.
Known property: tenure_weeks is constant (= early_tenure_weeks) across the
early table — the defining property of the regime, not a feature. The
published-bundle no-zero-variance check must exempt it for this task family
(noted for the validation harness, LTV-Pp).
Tests (19): tenure constant at anchor; eligibility = survival to anchor;
onboarding churners excluded; cohort difference vs calendar (post-anchor,
pre-obs churners); per-customer censoring leakage probe (delete each
customer's post-anchor events, features unchanged); targets recomputed off the
per-customer cutoff vs the invoice table; cold-start sparsity (NPS all-null at
4w; health aggregates over pre-anchor signals only); anchor + horizon +
mismatch + missing-obs validation; distortions leave targets and trap intact.
Scope note: the actual early-pLTV *task directory* + split export
(render/tasks.py) folds into LTV-Pn with the bundle/task writer, matching how
LTV-Pl deferred the calendar task-split writer. This PR delivers the snapshot
builder + recomputed targets.
Full suite 1790 passed / 51 skipped; ruff + mypy clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(lifecycle): disclose all degenerate early-pLTV columns [LTV-Pm]
Findings from hostile self-review of the early-pLTV snapshot PR.
1. CALENDAR BYTE-IDENTITY: PROVEN, not just claimed. The "calendar output
unchanged by the unification refactor" claim rested on derivation tests,
which a subtle reordering could pass. Verified the refactored builder
produces byte-identical calendar snapshots to main across all 5 motifs x
2 seeds, with and without difficulty distortions. No code change; the 30
LTV-Pl derivation tests remain the permanent guard.
2. INCOMPLETE DISCLOSURE OF DEGENERATE COLUMNS (the real finding). The PR
documented only tenure_weeks as constant in the early regime, but at a
short anchor MULTIPLE feature columns are dead by construction — confirmed
structural (every seed), not seed accidents:
- renewal_count: constant 0 for any anchor < 52w (first anniversary wk 52)
- last_nps_score: all-null for any anchor < 13w (first survey wk 13)
- weeks_since_last_payment_failure: near-degenerate (<=1 distinct value)
Shipping a builder while under-documenting that ~3 columns are dead in its
primary (4-week) configuration would mislead consumers and the validation
harness. Expanded the build_early_pltv_snapshot docstring and the roadmap
note to enumerate all of them with the cadence reason, flag the
shared-catalog design tension, and hand LTV-Pp the full exemption list /
LTV-Pn the drop-or-keep decision. New parametrized test pins the
structural set across seeds so reviving any column forces a conscious
update.
3. Added an early-regime trap-divergence test: the mrr_change_full_period
trap is *more* leaky here than in the calendar regime (at 4 weeks
mrr_change_at_snapshot is ~0 for >80% of rows while the trap captures the
whole future expansion path) — pinned so the pedagogically central column
can't silently stop diverging.
Full suite 1794 passed / 51 skipped; ruff + mypy clean; lead-scoring
distorted-snapshot hash still byte-identical (196bc45f…).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>1 parent c0abf4a commit df4fad8
4 files changed
Lines changed: 556 additions & 69 deletions
File tree
- docs/ltv
- leadforge/schemes/lifecycle
- tests/schemes/lifecycle
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
64 | 64 | | |
65 | 65 | | |
66 | 66 | | |
67 | | - | |
68 | | - | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
69 | 75 | | |
70 | 76 | | |
71 | 77 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
45 | 45 | | |
46 | 46 | | |
47 | 47 | | |
48 | | - | |
| 48 | + | |
49 | 49 | | |
50 | 50 | | |
51 | 51 | | |
| |||
232 | 232 | | |
233 | 233 | | |
234 | 234 | | |
235 | | - | |
236 | | - | |
237 | | - | |
238 | | - | |
239 | | - | |
240 | | - | |
| 235 | + | |
| 236 | + | |
| 237 | + | |
| 238 | + | |
| 239 | + | |
| 240 | + | |
| 241 | + | |
| 242 | + | |
| 243 | + | |
| 244 | + | |
| 245 | + | |
| 246 | + | |
| 247 | + | |
| 248 | + | |
| 249 | + | |
| 250 | + | |
| 251 | + | |
| 252 | + | |
| 253 | + | |
| 254 | + | |
| 255 | + | |
| 256 | + | |
| 257 | + | |
| 258 | + | |
| 259 | + | |
| 260 | + | |
| 261 | + | |
| 262 | + | |
| 263 | + | |
| 264 | + | |
241 | 265 | | |
242 | 266 | | |
243 | 267 | | |
| |||
0 commit comments