Files
Bike-Tinder/docs/RANKING.md

48 lines
4.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# RANKING — Bike-Tinder
The deck is ordered by predicted taste. Three rungs, in order: the hunt's existing rubric (v1), a cheap explainable boost from swipe clusters (v1.x), then a small learned model (Phase 3). Nothing ever becomes a black box.
## Signals
- Like = strong positive (+1). Maybe = weak positive (+0.4) on the *direction* — "right lane, wrong trim" (Glenn's read): the family/silhouette reads positive, the specific build is uncertain; maybes replenish via second looks + variant hunts. Nope = negative (−1). List-view edits count identically; the latest call wins (full history lives in `swipe_events`).
- Planned v2 signals: dwell time on card, "open listing" click-outs.
## Features (v1)
Per listing: family tier (from the hunt rubric), displacement (cc), price, year, km, km/year, dealer vs private, place/region, keyword flags from title + description (MVI/inspection, whitewalls, spokes, windshield, saddlebags, ABS, bobber/custom), color words (red is known lore), title tokens (make/model). Numeric features normalized; missing values neutral.
- Single-source v1 makes source/region features near-constant — kept in the schema for v2.
## Stage 1 — Cold start
Order by the hunt's existing rubric score (family tiers + MVI / whitewalls / windshield / spokes / ABS bonuses + price fit). This is already explainable (reason strings exist in the hunt system). Deck = rubric order until ≥ 20 swipes.
## Stage 1.5 — Cheap swipe boost (v1.x, post-launch)
Before any model exists, the importer can nudge `score` with plain swarm signals — e.g. +N when recent likes cluster in a family, small time-decay otherwise — recomputed each import, zero ML, fully explainable. Ships as v1.x if Glenn wants it; v1 itself stays pure rubric.
## Stage 2 — P3 taste engine (frozen 2026-11-10)
Deterministic, explainable, pure-Python. Learning reads `swipe_events` only.
**Event weights.** action: like +1.0 · maybe +0.4 · nope −1.0; round multiplier: round 1 ×1, round ≥2 **×2** (second strikes hit twice as hard — reaffirmations and flips both); recency: ×0.5^(age_days / 21). Event weight = product.
**Feature weights.** Feature set per listing: family · price band (`<3k, 3–5k, 5–8k, 8–12k, 12k+`) · displacement band (from title/desc) · year band · km band (when present) · each extras flag · seller type · silhouette keywords (bobber/custom/classic/cruiser) · color words · region. For each (feature, value): `raw = Σ event weights`; `shrunk = raw × n/(n+3)` — no single strike taboos a family.
**Score.** `taste_raw` = Σ shrunk weights over the listing's values; `taste_norm` = percentile rank among active listings (0–100). Blend: `w = min(0.85, N/60)` where N = total events; `points = clip( round( (1−w)·rubric + w·taste_norm + agent_adj ), 0, 100 )`. `agent_adj` = Σ profile adjustments (clamped ±10 per listing). Breakdown for the dossier: rubric share · top 3 ± taste features · agent lines — all source-labeled.
**Section recipe.** 25 cards = 2 wildcards + ≤5 returns + remainder new (points DESC). Constraints: ≤4 per family per section; ≥3 price bands when the pool allows; shortfalls fill from new.
**Wildcards.** Candidates: active, unseen, not in the last two sections. Exclusions: families with ≥3 cumulative nopes and net-negative weight. Prefer: different family AND different price band than the section's top new items. Score = `0.5·points + 0.5·novelty` (novelty = distance from the liked-profile centroid). Picks: 2 per section.
**Returns ("second looks").** Pool + caps per DATA-MODEL. Order: maybe > like > surprising nope (points ≥55 or net-positive family); rank within role by points DESC. A re-nope (round≥2 nope) rests the item permanently in v1.
**Guardrails.** Never hide (List untouched; ranking only reorders). The ramp keeps cold start on rubric; the engine must run with zero profile (w=0). Agent offline → compose on the last profile (source-labeled). **No LLM inside any request path or computed score** — the pass only writes the profile.
## Explainability
Each card can show up to 2 chips: top positive contributions for that bike ("low km for the money", "Vulcan family", "red", "MVI"). Chips derive from the live feature contributions; during cold start they come from the rubric's existing reasons.
## Evaluation
- Track: swipes/session, like-rate trend, maybe-rate, undo rate, and "liked-item rank" (how deep liked items sat — should trend shallow).
- P3 adds: hit rate (likes/decided · likes+maybes), returns re-strike rate, wildcard hit rate, flip count — surfaced in the Trends tab.
- Notifications (decided 2026-11-10): in-app "new since last visit" badge only — no Conduit pushes. A weekly digest remains possible later if asked for.
## Open decisions
- Maybe-handling — **resolved 2026-11-10 (Glenn):** weak positive on the direction ("right lane, wrong trim"); maybes replenish via second looks + variant hunts.
- Distance feature (map-based proximity) — likely v2.