Spaces:
Sleeping
Sleeping
| """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)), | |
| } | |