Files
oO/ml/experiments/sim/personas.py
alvis 2d7cf217a9 feat(ml): egreedy-v2 shadow policy — D=12 with profile features (#99)
Ship the scaffolding for #99 (phase B.3 of #81):

- ml/serving: add /score/egreedy/v2, /reward/egreedy/v2, /stats/egreedy/v2
  endpoints (D=12). New feature dims: completion/dismiss rates, mean dwell
  (clipped 10min), preferred-hour alignment (cosine, 1-dim), tip volume (log).
  Separate state file per user (_egreedy_v2.json). /reset clears v2 state too.
- ADR-0012: documents D=7→12 dimension change, normalization choices, shadow
  rollout protocol, and promotion gate (offline sim win per ADR-0002).
- recommender.ts: register egreedy-v2-shadow in shadow-policy map (disabled by
  default). When enabled, calls /score/egreedy/v2 fire-and-forget and publishes
  shadow:egreedy-v2-shadow serve signal. No reward to shadow — sim is the gate.
- sim runner/personas: personas carry synthetic profile_features per persona;
  _call_score/_call_reward thread profile_features through (None-safe for v1/linucb).
- 18 new Python tests; all 56 Python + 170 TS tests pass.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 10:00:38 +00:00

124 lines
3.9 KiB
Python
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.
"""Synthetic user personas for simulation."""
import math
from dataclasses import dataclass
@dataclass
class Persona:
name: str
description: str
# Feature preference weights — used by deterministic judge
prefers_high_priority: float # 01: scales response to priority
prefers_overdue: float # 01: scales response to overdue tasks
morning_active: bool # higher engagement hours 610
evening_active: bool # higher engagement hours 1822
recency_bias: float # 01: prefers recently-due tasks
# Synthetic profile features for egreedy-v2 sim (ADR-0012).
# Values represent what a typical user of this persona would have
# accumulated after a few weeks of app use.
_completion_rate: float = 0.3
_dismiss_rate: float = 0.2
_mean_dwell_ms: float = 60_000.0 # ms
_preferred_hour: float = 12.0 # 023
_tip_volume_30d: float = 15.0
def profile_features(self, now_hour: int | None = None) -> dict:
"""Return profile_features dict compatible with the ml/serving API."""
return {
"completion_rate_30d": self._completion_rate,
"dismiss_rate_30d": self._dismiss_rate,
"mean_dwell_ms_30d": self._mean_dwell_ms,
"preferred_hour": self._preferred_hour,
"tip_volume_30d": self._tip_volume_30d,
}
PERSONAS: list[Persona] = [
Persona(
name="deadline-driven",
description=(
"Responds urgently to overdue and high-priority tasks. "
"Most active in the morning. Dismisses low-priority tips."
),
prefers_high_priority=0.9,
prefers_overdue=0.85,
morning_active=True,
evening_active=False,
recency_bias=0.3,
_completion_rate=0.55,
_dismiss_rate=0.10,
_mean_dwell_ms=45_000.0,
_preferred_hour=8.0,
_tip_volume_30d=22.0,
),
Persona(
name="evening-relaxed",
description=(
"Reviews tasks in the evenings. Neutral on priority. "
"Snoozes morning recommendations."
),
prefers_high_priority=0.5,
prefers_overdue=0.4,
morning_active=False,
evening_active=True,
recency_bias=0.5,
_completion_rate=0.30,
_dismiss_rate=0.25,
_mean_dwell_ms=90_000.0,
_preferred_hour=20.0,
_tip_volume_30d=12.0,
),
Persona(
name="low-priority-first",
description=(
"Clears small tasks first. Snoozes urgent items until deadline. "
"Morning person."
),
prefers_high_priority=0.2,
prefers_overdue=0.6,
morning_active=True,
evening_active=False,
recency_bias=0.7,
_completion_rate=0.40,
_dismiss_rate=0.15,
_mean_dwell_ms=30_000.0,
_preferred_hour=9.0,
_tip_volume_30d=18.0,
),
Persona(
name="consistent-responder",
description=(
"Engages consistently across hours and days. "
"Acts on helpful tips regardless of priority."
),
prefers_high_priority=0.6,
prefers_overdue=0.6,
morning_active=True,
evening_active=True,
recency_bias=0.5,
_completion_rate=0.50,
_dismiss_rate=0.10,
_mean_dwell_ms=60_000.0,
_preferred_hour=12.0,
_tip_volume_30d=30.0,
),
Persona(
name="overdue-ignorer",
description=(
"Avoids overdue tasks (stress avoidance). "
"Focuses on future-due, high-priority items. Evening person."
),
prefers_high_priority=0.8,
prefers_overdue=0.1,
morning_active=False,
evening_active=True,
recency_bias=0.2,
_completion_rate=0.20,
_dismiss_rate=0.40,
_mean_dwell_ms=120_000.0,
_preferred_hour=19.0,
_tip_volume_30d=10.0,
),
]