Definitions of every business metric — the contract between analytics output and what reviewers expect. Headline metric is conversion rate. Facts about the data are in [[GROUND_TRUTH]]. Update whenever business logic changes.
(TBD)= pending confirmation.
- Track — one tracked person across frames (per camera), a
track_id. - Session — one customer visit; bounded by entry and exit. The unit for funnel/conversion (sessions, never raw detections, to avoid double counting — rubric-critical).
- Zone — a named store region from the floor plan ([[GROUND_TRUTH]] §4): entrance, skincare_aisle,
makeup_aisle, foh_center, checkout, accessories, stockroom (staff). Each camera maps to one zone:
by default the hand-mapped
primary_zone, or — for product cameras withVLM_ENABLED=true— a VLM-labelled zone (Gemini or Groq) read from the shelves (entrance/checkout/stockroom stay role-known; ADR-0027). On Store_2 the VLM relabelled thezonecammakeup_aisle → skincare_aisle. - Visitor — one visit session, identified by a
visitor_id(Re-ID). Re-entries keep the same id. - Transaction — one POS order from the CSV (
distinct order_id). 24 on 10-Apr-2026.
- Definition (per [[SPEC]]):
conversion_rate = converted visitors ÷ unique visitorsover a session window. Staff excluded. - Converted visitor (POS correlation rule): a visitor whose session was in the billing zone
within the 5-minute window before a POS transaction timestamp (same store) counts as converted.
No
customer_id— correlation is time-window + store only. - unique visitors: distinct non-staff
visitor_ids, counted from every camera (Re-ID de-dups a shopper seen on several cameras / on return into one visitor), but only for a solid track (ADR-0029): sustained presence (min_zone_dwell), on the walkable floor (floor_regionwhere calibrated), a large-enough box (min_detection_box_fracdrops tiny far/reflection blobs), and — on any camera with anentrance_line— only the store-interior side (mall-corridor pass-by is discarded by the line). So the entrance camera now contributes interior visitors too (refining ADR-0011, which had excluded it wholesale; the calibrated line keeps the corridor traffic out). Staff (ADR-0009/0027) are excluded. This is the conversion denominator. (Earlier validated count on the Store_1 clip was 2 customers + 3 staff under the floor-cam-only rule; re-validate on the next full run now that counting spans all cameras — ADR-0029.) - POS source (corrected dataset, [[GROUND_TRUTH]] §2): 7-col
POS - sample transactions.csv→ a transaction = one basket keyed"{store_id}_{order_date}_{order_time}"(order_idis per-line-item, not the basket key),timestampfromorder_date+order_time(local → UTC viastore_timezone),gmv= Σtotal_amountper basket,brand= the basket's dominantbrand_name. 24 transactions, store ST1008 only (Store_1; Store_2 has no POS).pos_loader.pywas reworked for this format (done). - ⚠ Window caveat (handled, ADR-0012): clips (~2 min) vs CSV (full day) differ, and no sale falls in
the clip window. We do not divide mismatched windows: we report the honest clip conversion (0%,
data_confidence="low") and demonstrate the correlation on a comparable window (demo_conversion.py). The 24 real sales still power day-level KPIs (total ₹34,331.71 viatotal_amount; basket, peak hour 12:00/19:00) viapos_day_metrics.
- Definition:
ENTRY/EXITcount the flow across the entrance threshold in a window. - Rule: an
ENTRYfires when a track's foot-point crosses the entrance line inward on CAM3 (calibrated, centre-left door);EXITon outward crossing. Debounced for on-line flicker. - ⚠ On short clips this is ≈0 because most shoppers entered before the window — so the unique-visitor count (above), not entrance-crossings, is the basis for conversion (ADR-0007). ENTRY/EXIT remain valuable as real flow signals and would scale on longer/live feeds.
- Do NOT count mall pass-by: people walking the mall corridor past the storefront are not visitors; the entrance line is placed on the actual door, not the busiest motion (ADR-0006).
- Must handle (Detection bucket, 30 marks): re-entry, staff, group entry — see [[EDGE_CASES]].
- Start: first detection of the track after entry.
- End: track lost >
session_timeoutOR outward exit crossing. - Re-entry rule: a returning visitor (matched by Re-ID) keeps the same
visitor_idand emitsREENTRY, never a secondENTRY— so footfall/conversion are not inflated. See [[EDGE_CASES]].
Two signals, VLM-first with a heuristic fallback (ADR-0009/0027/0032):
- Per-store uniform-colour heuristic (always-available fallback):
is_stafffrom a uniform-colour score — the fraction of in-range HSV pixels in the person crop, reusing the Re-ID crop. The colour is per store viastaff_heuristic_color(staff.pyCOLOR_HEURISTICSregistry): Store_1 =black(both upper+lower body, takes the min — a full black uniform scores high), Store_2 =pink(upper body only — staff wear bright pink polos). A track is staff when its mean score ≥staff_uniform_threshold(default 0.50).staff_heuristic_color=Nonedisables the heuristic (VLM-only). Adding a new uniform colour = one entry in the registry + the store'sstaff_heuristic_color. - VLM (primary when
VLM_ENABLED=true, ADR-0027/0031): a Gemini or Groq call (once pervisitor_id, offline only) decides staff/customer and overrides the heuristic when it clearsvlm_staff_min_confidence(else the heuristic stands). A per-storestaff_uniform_hintis injected into the prompt (Store_2: "staff wear bright solid pink polos"). Off by default, cached, gate-safe. - Aggregation: the API treats a visitor as staff if any of their events is flagged.
- Excluded from unique visitors, conversion, funnel, heatmap — staff are not customers.
- Limits: on steep overhead CCTV uniforms/lanyards are hard to see, so the staff/customer split is the weakest link — a same-colour customer can be misflagged, and the VLM verdict is crop-sensitive ([[GROUND_TRUTH]] §1). The total head-count is the more reliable output. See [[EDGE_CASES]] #2. (The old Store_1 stockroom cam CAM4 was staff-only; it is no longer in the corrected dataset.)
- Ordered sequence of zones a visitor passes through with timestamps. Append a zone only when dwell
≥
min_zone_dwellto filter pass-through noise.
- Stages (per [[SPEC]]):
Entry → Zone Visit → Billing Queue → Purchase, with counts + drop-off %. - Session is the unit; NO double counting: each
visitor_idcounts at most once per stage; re-entries do not double-count a visitor. - Drop-off:
drop_off(stage_n) = 1 − visitors(stage_n) ÷ visitors(stage_{n-1}). - Purchase: visitor satisfies the POS correlation rule above.
- Dwell: total contiguous time a visitor's mapped position (foot point) is inside a zone.
- Engagement: a visitor is counted as engaging a zone when dwell ≥
min_engagement_dwell. - A
ZONE_DWELLevent is emitted every 30s of continuous presence in a zone (see [[EVENT_SCHEMA]]). - Report per-zone visitor counts and avg/total dwell (feeds the heatmap).
BILLING_QUEUE_JOIN: emitted when a non-staff visitor enters the checkout zone on CAM5 (BillingTracker, driven off the CAM5ZONE_ENTER). queue_depth = number of customers in the checkout zone including the joiner (set inmetadata.queue_depth). Staff are excluded from the queue.BILLING_QUEUE_ABANDON: derived inconversion.py(not the detector — it needs POS): a billing visitor with no POS transaction in the correlation window. abandonment rate = abandons ÷ billing-zone customers.- Staff at the per-event vs visitor level: a CAM5 track that dips below the staff-darkness threshold can
emit a JOIN with
is_staff=false, but if the visitor is overall staff (any event flagged) the conversion step excludes them — so staff never pollute the billing/customer counts. - Honest clip result: customers browse CAM2, none reach checkout → 0 billing customers, conversion 0%
with
data_confidence="low"(the window mismatch). The mechanism is demonstrated on a comparable window (scripts/demo_conversion.py,POS_DEMO_ALIGNMENT).
- Per-zone distinct-customer visit frequency + average dwell, normalised 0–100 (busiest zone = 100) for grid rendering. Staff excluded.
- Include a
data_confidence="low"flag when <conversion_low_sample_threshold(20) customers.
- Queue spike: billing
queue_depth(staff-excluded) ≥anomaly_queue_depth_warn⇒ WARN, ≥anomaly_queue_depth_critical⇒ CRITICAL. Suggested action: open another till. - Conversion drop: conversion below
anomaly_conversion_baseline × (1 − anomaly_conversion_drop_pct). ⚠ We have one day of data, not a 7-day average, so the baseline is a documented config target. The check fires only atdata_confidence="ok"; under low sample it emits INFO — it never cries wolf on the 2-min clip. - Dead zone: a monitored customer zone (from the store's config, per
store_id) with no visit foranomaly_dead_zone_minutesduring open hours (store_open_hour–store_close_hour). Span-guarded: only evaluated when the observed window is at least that long; otherwise INFO ("window too short"). - Each:
severityINFO/WARN/CRITICAL + a humansuggested_action. All compute from input (no hardcoding); honest dormancy over fabricated alerts is deliberate (integrity cap + reviewer trust).
/healthreports per-storelast_event_at,lag_seconds,stale_feed. Freshness is recording-relative by default (lag vs the latest ingested event, so a replayed clip reads healthy);health_strict_now=truecompares to real wall-clock for live ops.STALE_FEEDwhen lag >health_stale_feed_minutes.status=degradedif the DB is unreachable or any feed is stale.
transaction_count(distinctorder_timebaskets),total_gmv(Σtotal_amount),avg_basket,peak_hour,top_brand(busiestbrand_name), andtop_department(busiest category).top_departmentrolls brands up via a curated brand → department taxonomy (shelfsense_common/departments.py, ADR-0025): makeup · skincare · haircare · bath_and_body · personal_care · fragrance · accessories · other (unmapped/own-label). A basket's department follows its dominant brand; the rollup excludesotherso a meaningful category wins. Reference data — the output still varies with real sales (no hardcoding). On the real file: top brand Faces Canada, top department makeup.
- Unique visitors, conversion rate, avg session duration, avg dwell per zone, top zones, basket value (from POS), top brand + top department, abandonment rate, queue depth.
| Param | Meaning | Default (TBD) |
|---|---|---|
min_zone_dwell |
min dwell to record a zone in a journey | 2 s |
min_engagement_dwell |
min dwell to count zone engagement | 3 s |
session_timeout |
track-lost duration that ends a session | 30 s |
reentry_window |
gap within which a re-entry is the same visit | 120 s |
staff_uniform_threshold |
mean uniform-colour score ≥ ⇒ is_staff (ADR-0009/0032) |
0.50 |
staff_heuristic_color |
per-store uniform colour for the heuristic (black/pink/None) |
black |
staff_uniform_v_max |
HSV Value (0–255) ceiling for the black heuristic's near-black test |
70 |
staff_presence_fallback |
also flag long-present tracks as staff (off by default) | false |
reid_max_distance |
appearance distance to merge tracks; per-store override (ST1009=0.35) | 0.55 |
min_zone_dwell (per-store) |
per-store override of the dwell above (ST1009=800 ms) | 2000 ms |
detector_imgsz |
YOLO inference size; per-store override (ST1008=480, global 768) | 768 |
pos_correlation_window_ms |
billing-zone-before-a-sale window for "converted" (ADR-0012) | 300000 (5 min) |
conversion_low_sample_threshold |
< N unique customers ⇒ data_confidence="low" |
20 |
store_timezone |
tz for POS order_date+order_time → UTC |
Asia/Kolkata |
anomaly_queue_depth_warn / _critical |
checkout-customer depth ⇒ WARN / CRITICAL queue spike (2.7) | 3 / 5 |
anomaly_conversion_baseline |
documented target conversion rate (no 7-day history) — drop baseline | 0.15 |
anomaly_conversion_drop_pct |
fire conversion-drop when rate ≤ baseline·(1−this), at ok confidence only | 0.30 |
anomaly_dead_zone_minutes |
zone silent this long during open hours ⇒ dead zone (span-guarded) | 30 min |
store_open_hour / store_close_hour |
store-local trading window for dead-zone | 12 / 22 |
health_stale_feed_minutes |
feed lag beyond which a store is STALE_FEED |
10 min |
health_strict_now |
false ⇒ freshness vs latest event (recording-relative); true ⇒ vs real now | false |
All thresholds via environment variables (no hardcoding). See [[EVENT_SCHEMA]] for how these become events and [[API_SPEC]] for how they surface.