goon/scripts/edge_replay_features.py
goon-foss 251032a13b 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>
2026-10-02 11:48:17 +02:00

95 lines
4.6 KiB
Python

"""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)