From 251032a13b65755776c6f384e8bb2012f33817ad Mon Sep 17 00:00:00 2001 From: goon-foss Date: Fri, 2 Oct 2026 11:48:17 +0200 Subject: [PATCH] chore(edge): skrypty odtworzenia i oceny punktacji feedu edge_replay_features.py liczy na prodzie cechy znane w chwili kazdego wyswietlenia, edge_tuning_analysis.py ocenia formuly (AUC w obrebie urzadzenia, podzial w czasie). Na nich dobrana punktacja z 4666fcb. Co-Authored-By: Claude Opus 5.5 --- scripts/edge_replay_features.py | 95 +++++++++++++++++++++++++++++++++ scripts/edge_tuning_analysis.py | 82 ++++++++++++++++++++++++++++ 2 files changed, 177 insertions(+) create mode 100644 scripts/edge_replay_features.py create mode 100644 scripts/edge_tuning_analysis.py diff --git a/scripts/edge_replay_features.py b/scripts/edge_replay_features.py new file mode 100644 index 0000000..7a43ea1 --- /dev/null +++ b/scripts/edge_replay_features.py @@ -0,0 +1,95 @@ +"""Odtworzenie uczenia feedu Edge zdarzenie po zdarzeniu (bez zagladania w przyszlosc). + +Dla kazdego wyswietlenia (complete/skip/impression/autoswipe) zapisuje cechy znane W TEJ +CHWILI: skladniki obecnej punktacji z wag urzadzenia sprzed zdarzenia oraz liczniki klipu +z wczesniejszych zdarzen wszystkich urzadzen. Wynik CSV na stdout. + +Uzyte do przestrojenia punktacji 2026-10-02 (patrz app/clips_feed.py, score_candidate). +Wybor tagow na starcie (onboarding) nie jest zdarzeniem, wiec tu go brakuje. + + docker compose exec -T worker sh -c "cd /srv && python3 scripts/edge_replay_features.py | gzip -c" > edge.csv.gz +Analiza lokalnie: python scripts/edge_tuning_analysis.py edge.csv (pandas + scikit-learn). +""" +import csv +import math +import sys +from collections import defaultdict +from datetime import timedelta + +from sqlalchemy import text + +from app import clips_feed as cf +from app.db import session_scope + +with session_scope() as s: + ev = s.execute(text(""" + select device_id, clip_id, kind, dwell_ms, reason, slot, created_at + from clip_feed_events where clip_id is not null order by created_at""")).all() + ids = list({e.clip_id for e in ev}) + clips = {} + for i in range(0, len(ids), 5000): + for r in s.execute(text("""select id, creator, likes, published_at, niches, duration_sec, width, height, + has_audio, creator_verified + from clips where id = any(:ids)"""), {"ids": ids[i:i + 5000]}).all(): + clips[r.id] = r + tags = defaultdict(list) + for i in range(0, len(ids), 5000): + for cid, t in s.execute(text("select clip_id, tag from clip_tags where clip_id = any(:ids)"), {"ids": ids[i:i + 5000]}).all(): + tags[cid].append(t) + n_all = s.execute(text("select count(*) from clips")).scalar() + tag_df = dict(s.execute(text("select tag, count(*) from clip_tags group by tag")).all()) +idf = {t: math.log((n_all + 1) / (n + 1)) for t, n in tag_df.items()} +print(f"zdarzen {len(ev)}, klipow {len(clips)}, tagow {len(idf)}", file=sys.stderr) + +aff = defaultdict(cf.Affinity) +last_day = {} +dev_n = defaultdict(int) +# liczniki klipu z wczesniejszych zdarzen (wszystkie urzadzenia) +c_seen = defaultdict(int) +c_comp = defaultdict(int) +c_skip = defaultdict(int) +c_like = defaultdict(int) +liked = set((e.device_id, e.clip_id) for e in ev if e.kind == "like") + +w = csv.writer(sys.stdout) +w.writerow(["dev", "ts", "slot", "kind", "dwell", "liked", "tag_sum", "tag_pos", "tag_n", "niche_sum", "creator_w", + "tag_idf_sum", "tag_idf_pos", "pop", "fresh", "dur", "vertical", "audio", "verified", "c_seen", "c_comp", "c_skip", "c_like", "dev_n"]) +OUTCOME = {"complete", "skip", "impression", "autoswipe"} +for e in ev: + c = clips.get(e.clip_id) + if c is None: + continue + a = aff[e.device_id] + day = e.created_at.date() + prev = last_day.get(e.device_id) + if prev is not None and day > prev: + for _ in range((day - prev).days): + a.tags = cf.decay_weights(a.tags) + a.niches = cf.decay_weights(a.niches) + a.creators = cf.decay_weights(a.creators) + last_day[e.device_id] = day + t = tags.get(e.clip_id, []) + niches = list(c.niches or []) + if e.kind in OUTCOME: + ts_vals = [a.tags.get(x, 0.0) for x in t] + fresh = int(bool(c.published_at and c.published_at >= e.created_at - timedelta(days=cf.FRESH_DAYS))) + vert = int(bool(c.width and c.height and c.height > c.width)) + w.writerow([e.device_id[:12], e.created_at.isoformat(), e.slot or "", e.kind, e.dwell_ms or 0, + int((e.device_id, e.clip_id) in liked), + round(sum(ts_vals), 3), sum(1 for v in ts_vals if v > 0), len(t), + round(sum(a.niches.get(n, 0.0) for n in niches), 3), + round(a.creators.get(c.creator or "", 0.0), 3), + round(sum(a.tags.get(x, 0.0) * idf.get(x, 0.0) for x in t), 3), + round(sum(idf.get(x, 0.0) for x in t if a.tags.get(x, 0.0) > 0), 3), + round(math.log1p(max(c.likes or 0, 0)), 3), fresh, round(c.duration_sec or 0, 1), vert, + int(bool(c.has_audio)), int(bool(c.creator_verified)), + c_seen[e.clip_id], c_comp[e.clip_id], c_skip[e.clip_id], c_like[e.clip_id], dev_n[e.device_id]]) + c_seen[e.clip_id] += 1 + if e.kind == "complete": + c_comp[e.clip_id] += 1 + elif e.kind == "skip": + c_skip[e.clip_id] += 1 + dev_n[e.device_id] += 1 + elif e.kind == "like": + c_like[e.clip_id] += 1 + cf.apply_event(a, kind=e.kind, dwell_ms=e.dwell_ms, reason=e.reason, tags=t, niches=niches, creator=c.creator) diff --git a/scripts/edge_tuning_analysis.py b/scripts/edge_tuning_analysis.py new file mode 100644 index 0000000..ebd4a1d --- /dev/null +++ b/scripts/edge_tuning_analysis.py @@ -0,0 +1,82 @@ +"""Ocena punktacji feedu Edge na cechach z scripts/edge_replay_features.py. + +Trening na 70% najstarszych wyswietlen, test na reszcie. Glowna miara: AUC W OBREBIE +URZADZENIA (czy wyzszy wynik u tego samego urzadzenia znaczy lepsza reakcje), bo globalne +AUC miesza aktywnych z nowymi. Dwie etykiety: lajk albo >= 10 s ogladania (odporna na +dlugosc klipu) oraz dokonczenie albo lajk. F5 to formula wdrozona 2026-10-02. +Wymaga pandas i scikit-learn (lokalnie, nie w kontenerze). +""" +import sys + +import numpy as np +import pandas as pd +from sklearn.linear_model import LogisticRegression +from sklearn.metrics import roc_auc_score + +df = pd.read_csv(sys.argv[1], parse_dates=["ts"]) +A = 10.0 +P_LIKE, P_SKIP = 0.03, 0.5 +df["like_rate"] = (df.c_like + A * P_LIKE) / (df.c_seen + A) +df["skip_rate"] = (df.c_skip + A * P_SKIP) / (df.c_seen + A) +df["popul"] = df["pop"] +slog = lambda x: np.sign(x) * np.log1p(np.abs(x)) # noqa: E731 +for c in ["tag_sum", "tag_idf_sum", "creator_w", "niche_sum"]: + df["s_" + c] = slog(df[c]) + + +def within(d, col): + num = den = 0.0 + for _, g in d.groupby("dev"): + if len(g) < 30 or g.y.nunique() < 2 or g[col].nunique() < 2: + continue + num += roc_auc_score(g.y, g[col]) * len(g) + den += len(g) + return num / den if den else float("nan") + + +def labelled(kind): + if kind == "dwell": + good = (df.liked == 1) | (df.dwell >= 10000) + else: + good = (df.kind == "complete") | (df.liked == 1) + bad = df.kind == "skip" + d = df[good | bad].copy() + d["y"] = good[good | bad].astype(int) + return d.sort_values("ts") + + +def cur(d): + return d.tag_sum + 2 * d.niche_sum + 3 * d.creator_w + 0.3 * d.popul + 0.5 * d.fresh + + +def formula(d, w): + return (w["tag"] * d.s_tag_idf_sum + w["creator"] * d.s_creator_w + w["pop"] * d.popul + + w["like"] * (d.like_rate - P_LIKE) - w["skip"] * (d.skip_rate - P_SKIP)) + + +for kind in ("dwell", "complete"): + d = labelled(kind) + cut = int(len(d) * 0.7) + tr, te = d.iloc[:cut].copy(), d.iloc[cut:].copy() + print(f"\n===== etykieta {kind}: trening {len(tr)}, test {len(te)}") + print(" AUC w obrebie urzadzenia (test):") + for f in ["s_tag_sum", "s_tag_idf_sum", "tag_idf_pos", "s_creator_w", "s_niche_sum", "popul", "like_rate"]: + print(f" {f:14s} {within(te, f):.3f}") + cols = ["s_tag_idf_sum", "s_creator_w", "s_niche_sum", "popul", "like_rate", "skip_rate"] + m = LogisticRegression(max_iter=3000).fit(tr[cols], tr.y) + coef = dict(zip(cols, m.coef_[0])) + base = coef["s_tag_idf_sum"] + print(" wspolczynniki w jednostkach tagu:", {c: round(v / base, 2) for c, v in coef.items()}) + te["cur"] = cur(te) + te["model"] = m.decision_function(te[cols]) + for name, w in { + "F1": {"tag": 1.0, "creator": 1.0, "pop": 0.3, "like": 20.0, "skip": 2.0}, + "F2": {"tag": 1.0, "creator": 1.5, "pop": 0.5, "like": 30.0, "skip": 3.0}, + "F3": {"tag": 1.0, "creator": 1.0, "pop": 0.5, "like": 40.0, "skip": 4.0}, + "F4": {"tag": 1.0, "creator": 2.5, "pop": 0.8, "like": 120.0, "skip": 15.0}, + }.items(): + te[name] = formula(te, w) + print(" porownanie (test): AUC globalnie / w obrebie urzadzenia") + te["F5"] = formula(te, {"tag": 1.0, "creator": 2.5, "pop": 0.8, "like": 120.0, "skip": 15.0}) + 0.25 * te.s_niche_sum + 0.3 * te.fresh + for f in ["cur", "model", "F1", "F2", "F3", "F4", "F5"]: + print(f" {f:6s} {roc_auc_score(te.y, te[f]):.3f} / {within(te, f):.3f}")