Fastwhisper / app /stats_math.py
Mbonea's picture
Deploy Habit Journal backend S0-S10 to Hugging Face Space.
990895d
Raw
History Blame Contribute Delete
9.33 kB
"""Pure probability and ranking math for log and daily statistics.
No I/O: callers pass entry/daily dicts; this module only aggregates.
"""
from __future__ import annotations
from collections import defaultdict
from typing import Any
def _normalize_remedy(value: str) -> str:
return " ".join(value.strip().lower().split())
def intensity_bucket(intensity: int) -> str:
"""Map intensity 1-10 into coarse buckets."""
if intensity <= 3:
return "1-3"
if intensity <= 6:
return "4-6"
return "7-10"
def scored_entries(entries: list[dict[str, Any]]) -> list[dict[str, Any]]:
"""Exclude pending outcomes from probability denominators."""
return [e for e in entries if e.get("result") != "pending"]
def outcome_histogram(entries: list[dict[str, Any]]) -> dict[str, int]:
"""Count outcomes including pending."""
counts = {"worked": 0, "partial": 0, "failed": 0, "pending": 0}
for entry in entries:
result = entry.get("result")
if result in counts:
counts[result] += 1
return counts
def by_remedy(
entries: list[dict[str, Any]],
*,
min_n: int,
shrink_k: float,
) -> list[dict[str, Any]]:
"""Compute per-remedy n, p_worked, p_helped, and shrinkage rank."""
scored = scored_entries(entries)
totals: dict[str, int] = defaultdict(int)
worked: dict[str, int] = defaultdict(int)
helped: dict[str, int] = defaultdict(int)
for entry in scored:
key = _normalize_remedy(str(entry.get("remedy") or ""))
if not key:
continue
totals[key] += 1
result = entry.get("result")
if result == "worked":
worked[key] += 1
helped[key] += 1
elif result == "partial":
helped[key] += 1
rows: list[dict[str, Any]] = []
for key, n in totals.items():
if n < min_n:
continue
p_worked = worked[key] / n
p_helped = helped[key] / n
rank = p_helped * (n / (n + shrink_k))
rows.append(
{
"key": key,
"n": n,
"p_worked": p_worked,
"p_helped": p_helped,
"rank": rank,
}
)
rows.sort(key=lambda row: (-row["rank"], -row["n"], row["key"]))
return rows
def by_emotion(entries: list[dict[str, Any]]) -> list[dict[str, Any]]:
"""Emotion multi-label buckets with p_helped among scored uses."""
totals: dict[str, int] = defaultdict(int)
helped: dict[str, int] = defaultdict(int)
for entry in scored_entries(entries):
result = entry.get("result")
is_helped = result in ("worked", "partial")
for emotion in entry.get("emotions") or []:
key = str(emotion).strip().lower()
if not key:
continue
totals[key] += 1
if is_helped:
helped[key] += 1
rows = [
{
"key": key,
"n": n,
"p_helped": (helped[key] / n) if n else 0.0,
}
for key, n in totals.items()
]
rows.sort(key=lambda row: (-row["n"], row["key"]))
return rows
def by_tag(entries: list[dict[str, Any]]) -> list[dict[str, Any]]:
"""Tag buckets with failure rate among scored uses."""
totals: dict[str, int] = defaultdict(int)
failed: dict[str, int] = defaultdict(int)
for entry in scored_entries(entries):
is_failed = entry.get("result") == "failed"
for tag in entry.get("tags") or []:
key = str(tag).strip().lower()
if not key:
continue
totals[key] += 1
if is_failed:
failed[key] += 1
rows = [
{
"key": key,
"n": n,
"p_failed": (failed[key] / n) if n else 0.0,
}
for key, n in totals.items()
]
rows.sort(key=lambda row: (-row["p_failed"], -row["n"], row["key"]))
return rows
def by_intensity_bucket(entries: list[dict[str, Any]]) -> list[dict[str, Any]]:
"""Intensity bucket counts and helped rates."""
totals = {"1-3": 0, "4-6": 0, "7-10": 0}
helped = {"1-3": 0, "4-6": 0, "7-10": 0}
for entry in scored_entries(entries):
try:
intensity = int(entry.get("intensity", 0))
except (TypeError, ValueError):
continue
if intensity < 1 or intensity > 10:
continue
bucket = intensity_bucket(intensity)
totals[bucket] += 1
if entry.get("result") in ("worked", "partial"):
helped[bucket] += 1
return [
{
"key": key,
"n": totals[key],
"p_helped": (helped[key] / totals[key]) if totals[key] else 0.0,
}
for key in ("1-3", "4-6", "7-10")
]
def corn_ok(row: dict[str, Any]) -> bool:
"""True when corn sessions are within the delay policy."""
sessions = int(row.get("corn_sessions") or 0)
delay_ok = bool(row.get("delay_ok", True))
return sessions == 0 or (sessions <= 1 and delay_ok)
def daily_rates(daily_rows: list[dict[str, Any]]) -> dict[str, float]:
"""Aggregate daily scoreboard rates for a set of days."""
n = len(daily_rows)
if n == 0:
return {
"p_brick_done": 0.0,
"p_corn_ok": 0.0,
"p_no_fc": 0.0,
"p_rerun_clean": 0.0,
"p_court_closed": 0.0,
"avg_points": 0.0,
}
brick = sum(1 for row in daily_rows if row.get("brick_done"))
corn = sum(1 for row in daily_rows if corn_ok(row))
no_fc = sum(1 for row in daily_rows if row.get("daydream") != "fc")
rerun = sum(1 for row in daily_rows if row.get("rerun") == "clean")
court = sum(1 for row in daily_rows if row.get("court") == "closed")
avg_points = sum(float(row.get("points") or 0) for row in daily_rows) / n
return {
"p_brick_done": brick / n,
"p_corn_ok": corn / n,
"p_no_fc": no_fc / n,
"p_rerun_clean": rerun / n,
"p_court_closed": court / n,
"avg_points": avg_points,
}
def helped_tags_by_remedy(entries: list[dict[str, Any]]) -> dict[str, set[str]]:
"""Tags that appear on helped uses of each remedy."""
tags_by_remedy: dict[str, set[str]] = defaultdict(set)
for entry in scored_entries(entries):
if entry.get("result") not in ("worked", "partial"):
continue
key = _normalize_remedy(str(entry.get("remedy") or ""))
if not key:
continue
for tag in entry.get("tags") or []:
token = str(tag).strip().lower()
if token:
tags_by_remedy[key].add(token)
return tags_by_remedy
def match_score(current_tags: set[str], remedy_tags: set[str]) -> float:
"""Fraction of current tags that match a remedy's helped-tag set."""
if not current_tags:
return 0.0
return len(current_tags & remedy_tags) / max(len(current_tags), 1)
def server_picks(
entries: list[dict[str, Any]],
current_tags: list[str] | set[str],
*,
min_n: int,
shrink_k: float,
match_alpha: float,
limit: int = 5,
) -> list[dict[str, Any]]:
"""Rank remedies by shrinkage + optional tag-match boost."""
tag_set = {str(tag).strip().lower() for tag in current_tags if str(tag).strip()}
remedy_tags = helped_tags_by_remedy(entries)
picks: list[dict[str, Any]] = []
for row in by_remedy(entries, min_n=min_n, shrink_k=shrink_k):
match = match_score(tag_set, remedy_tags.get(row["key"], set()))
pick = row["rank"] * (1.0 + match_alpha * match)
picks.append(
{
"remedy_key": row["key"],
"pick": pick,
"n": row["n"],
"p_helped": row["p_helped"],
"p_worked": row["p_worked"],
"rank": row["rank"],
"match": match,
}
)
picks.sort(key=lambda item: (-item["pick"], -item["n"], item["remedy_key"]))
return picks[:limit]
def data_thin(n_scored: int) -> bool:
"""True when scored history is too thin for strong coaching."""
return n_scored < 10
FORMULAS = {
"p_worked": "N(worked,r) / N(r); pending excluded",
"p_helped": "N(worked|partial,r) / N(r); pending excluded",
"rank": "p_helped * n/(n+k)",
"pick": "rank * (1 + alpha * match)",
"match": "|T intersect T_r| / max(|T|,1)",
"DATA_THIN": "n_scored < 10",
}
def build_stats(
entries: list[dict[str, Any]],
daily_rows: list[dict[str, Any]],
*,
min_n: int,
shrink_k: float,
generated_at: str,
) -> dict[str, Any]:
"""Assemble the /api/stats response payload."""
scored = scored_entries(entries)
return {
"n_entries_total": len(entries),
"n_entries_scored": len(scored),
"outcomes": outcome_histogram(entries),
"by_remedy": by_remedy(entries, min_n=min_n, shrink_k=shrink_k),
"by_emotion": by_emotion(entries),
"by_tag": by_tag(entries),
"by_intensity_bucket": by_intensity_bucket(entries),
"daily": daily_rates(daily_rows),
"formulas": FORMULAS,
"generated_at": generated_at,
"min_n": min_n,
"shrink_k": shrink_k,
"DATA_THIN": data_thin(len(scored)),
}