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 <noreply@anthropic.com>
This commit is contained in:
parent
6c2ebeecf5
commit
251032a13b
2 changed files with 177 additions and 0 deletions
95
scripts/edge_replay_features.py
Normal file
95
scripts/edge_replay_features.py
Normal file
|
|
@ -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)
|
||||
82
scripts/edge_tuning_analysis.py
Normal file
82
scripts/edge_tuning_analysis.py
Normal file
|
|
@ -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}")
|
||||
Loading…
Add table
Reference in a new issue