Files
Bike-Tinder/docs/RANKING.md

4.9 KiB
Raw Permalink Blame History

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.