sync 2026-10-23 13:31 IST | best val AUC 0.777500
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- README.md +2 -2
- parallel/lag/b_v2.py +7 -0
- parallel/lag/b_v3.py +8 -0
- parallel/lag/build.log +9 -0
- parallel/lag/cmp.py +14 -0
- parallel/lag/cmp2.py +14 -0
- parallel/lag/extra.py +79 -0
- parallel/lag/extra2.py +74 -0
- parallel/lag/lag_buckets.json +1 -0
- parallel/lag/lagtest.json +13 -0
- parallel/lag/lagtest.py +37 -0
- parallel/lag/r_v2.py +7 -0
- parallel/lag/r_v3.py +8 -0
- parallel/lag/repro_cmp.json +173 -0
- parallel/lag/repro_cmp2.json +188 -0
- parallel/lag/run2.log +12 -0
- parallel/report.md +135 -0
- parallel/score.log +102 -0
- parallel/score.py +29 -0
- parallel/score_val.json +100 -0
- parallel/src_snapshot/v2work/blend_val.py +44 -0
- parallel/src_snapshot/v2work/build_folds.py +27 -0
- parallel/src_snapshot/v2work/combo.py +85 -0
- parallel/src_snapshot/v2work/emb_exp.py +38 -0
- parallel/src_snapshot/v2work/explore.py +26 -0
- parallel/src_snapshot/v2work/extra.py +78 -0
- parallel/src_snapshot/v2work/extra2.py +73 -0
- parallel/src_snapshot/v2work/feats.py +225 -0
- parallel/src_snapshot/v2work/feats_pre_noeval.py +215 -0
- parallel/src_snapshot/v2work/prep.py +22 -0
- parallel/src_snapshot/v2work/starter_split.py +12 -0
- parallel/src_snapshot/v2work/starter_val.py +29 -0
- parallel/src_snapshot/v2work/train_val.py +39 -0
- parallel/src_snapshot/v2work/tt.py +59 -0
- parallel/src_snapshot/v2work/tt_exp.py +15 -0
- parallel/src_snapshot/v2work/v3feat_ne.py +290 -0
- parallel/src_snapshot/v2work/v3fold.py +18 -0
- parallel/src_snapshot/v2work/validate.py +18 -0
- parallel/src_snapshot/v2work/write_sub.py +27 -0
- parallel/src_snapshot/v3/ablation.py +22 -0
- parallel/src_snapshot/v3/baselines.py +38 -0
- parallel/src_snapshot/v3/blend.py +16 -0
- parallel/src_snapshot/v3/build_feats.py +29 -0
- parallel/src_snapshot/v3/features.py +285 -0
- parallel/src_snapshot/v3/leakcheck.py +45 -0
- parallel/src_snapshot/v3/metric.py +9 -0
- parallel/src_snapshot/v3/predict.py +53 -0
- parallel/src_snapshot/v3/prep.py +65 -0
- parallel/src_snapshot/v3/run_all.sh +21 -0
- parallel/src_snapshot/v3/segments.py +17 -0
README.md
CHANGED
|
@@ -10,7 +10,7 @@ tags: [ctr, lightgbm, tabular, recsys]
|
|
| 10 |
|
| 11 |
**No competition data is included** — no train/evaluation files, feature caches, parquet/duckdb, or row-level predictions/submissions. Only code, logs, metrics, charts and LightGBM model dumps.
|
| 12 |
|
| 13 |
-
_Last sync: 2026-10-23
|
| 14 |
|
| 15 |
## Submission log (`v2work/out/submission_log.csv`)
|
| 16 |
|
|
@@ -19,7 +19,7 @@ date,file,sha256,val_auc,public_auc
|
|
| 19 |
2026-10-23,submission_v2.csv,8a868ba5edac1b7fed695479cff9d2553f28d5cfa797805192d26a352930d1b4,0.7620,
|
| 20 |
2026-10-23,submission_v3.csv,466f3d64b8b273ac1c87c32a8d271761add9a925cb1f56979945a5d004cd2680,0.7775,
|
| 21 |
2026-10-23,submission_v2.csv,8a868ba5edac1b7fed695479cff9d2553f28d5cfa797805192d26a352930d1b4,SUPERSEDED (eval aggregates: pooled eval-row histories into item stats/cooc/SVD) - do not upload,
|
| 22 |
-
2026-10-23,submission_v3.csv.zip,
|
| 23 |
```
|
| 24 |
|
| 25 |
## v3 validation runs (`v3/out/results.jsonl`)
|
|
|
|
| 10 |
|
| 11 |
**No competition data is included** — no train/evaluation files, feature caches, parquet/duckdb, or row-level predictions/submissions. Only code, logs, metrics, charts and LightGBM model dumps.
|
| 12 |
|
| 13 |
+
_Last sync: 2026-10-23 13:31 IST · 290 files._
|
| 14 |
|
| 15 |
## Submission log (`v2work/out/submission_log.csv`)
|
| 16 |
|
|
|
|
| 19 |
2026-10-23,submission_v2.csv,8a868ba5edac1b7fed695479cff9d2553f28d5cfa797805192d26a352930d1b4,0.7620,
|
| 20 |
2026-10-23,submission_v3.csv,466f3d64b8b273ac1c87c32a8d271761add9a925cb1f56979945a5d004cd2680,0.7775,
|
| 21 |
2026-10-23,submission_v2.csv,8a868ba5edac1b7fed695479cff9d2553f28d5cfa797805192d26a352930d1b4,SUPERSEDED (eval aggregates: pooled eval-row histories into item stats/cooc/SVD) - do not upload,
|
| 22 |
+
2026-10-23,submission_v3.csv.zip (public score),466f3d64b8b273ac1c87c32a8d271761add9a925cb1f56979945a5d004cd2680,0.7775,0.788757
|
| 23 |
```
|
| 24 |
|
| 25 |
## v3 validation runs (`v3/out/results.jsonl`)
|
parallel/lag/b_v2.py
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import sys; sys.path.insert(0,'/workspace/huawei/parallel/src_snapshot/v2work')
|
| 2 |
+
from feats import load, first_events, build, log
|
| 3 |
+
D='/workspace/huawei/v2work'; DAY=86400; J20=1737331200
|
| 4 |
+
TE,TR,EE,ER,NI=load(D)
|
| 5 |
+
T_ev=first_events(TE,J20+5*DAY,2*DAY); T_rows=TR.join(T_ev.select('eid'),on='eid')
|
| 6 |
+
R=build(J20+4*DAY,T_ev,T_rows,TE,TR,NI)
|
| 7 |
+
R.drop('h','h_ts').write_parquet('/workspace/huawei/parallel/lag/feat/val25lag.parquet'); log('wrote',R.shape)
|
parallel/lag/b_v3.py
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""lag-1d: val25 targets (first event/user >= Jan25) with ALL stats frozen at Jan24 (v3feat_ne.build from snapshot)."""
|
| 2 |
+
import sys; sys.path.insert(0,'/workspace/huawei/parallel/src_snapshot/v2work'); sys.path.insert(1,'/workspace/huawei/v3')
|
| 3 |
+
from v3feat_ne import load, build, log
|
| 4 |
+
from split import J20, DAY, first_events
|
| 5 |
+
TE, TR, EE, ER, NI, NU = load('/workspace/huawei/v3/data')
|
| 6 |
+
T_ev = first_events(TE, J20 + 5 * DAY)
|
| 7 |
+
R = build(J20 + 4 * DAY, T_ev, TR.join(T_ev.select('eid'), on='eid', how='semi'), TE, TR, NI, NU)
|
| 8 |
+
R.write_parquet('/workspace/huawei/parallel/lag/feat_v3/val25lag.parquet'); log('wrote', R.shape)
|
parallel/lag/build.log
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
12:37:57 build C=1737676800 rows=439640 cols=170 38s
|
| 2 |
+
12:37:57 wrote (439640, 170)
|
| 3 |
+
12:38:02 pool 880430
|
| 4 |
+
12:38:05 cooc pairs 15164046
|
| 5 |
+
12:38:06 svd done
|
| 6 |
+
12:38:28 build C=1737676800 rows=439640 28s
|
| 7 |
+
12:38:29 wrote (439640, 125)
|
| 8 |
+
val25lag (439640, 39) 13s
|
| 9 |
+
val25lag (439640, 20) 33s
|
parallel/lag/cmp.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import polars as pl, numpy as np, json
|
| 2 |
+
V='/workspace/huawei/v2work'; L='/workspace/huawei/parallel/lag'; out={}
|
| 3 |
+
for d,a,b in (('feat_v3','val25','val25repro'),('feat','val25','val25repro'),('feat_x','val25','val25'),('feat_x2','val25','val25')):
|
| 4 |
+
A=pl.read_parquet(f'{V}/{d}/{a}.parquet'); B=pl.read_parquet(f'{L}/{d}/{b}.parquet')
|
| 5 |
+
B=A.select('rowid').join(B,on='rowid',how='left')
|
| 6 |
+
cols=[c for c in A.columns if c in B.columns and c!='rowid' and A[c].dtype.is_numeric()]
|
| 7 |
+
bad={}
|
| 8 |
+
for c in cols:
|
| 9 |
+
x=A[c].cast(pl.Float64).to_numpy(); y=B[c].cast(pl.Float64).to_numpy()
|
| 10 |
+
dif=np.nanmax(np.abs(x-y)) if np.isfinite(x-y).any() else 0.0
|
| 11 |
+
nanm=int((np.isnan(x)!=np.isnan(y)).sum())
|
| 12 |
+
if dif>1e-3*max(1,np.nanmax(np.abs(x))) or nanm: bad[c]=(float(dif),nanm)
|
| 13 |
+
out[d]=dict(ncols=len(cols),only_disk=[c for c in A.columns if c not in B.columns],n_diff=len(bad),diff=dict(list(bad.items())[:12]))
|
| 14 |
+
print(json.dumps(out,indent=1)); json.dump(out,open(f'{L}/repro_cmp.json','w'),indent=1)
|
parallel/lag/cmp2.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import polars as pl, numpy as np, json
|
| 2 |
+
V='/workspace/huawei/v2work'; L='/workspace/huawei/parallel/lag'; d0=json.load(open(f'{L}/repro_cmp.json')); out={}
|
| 3 |
+
for d,b in (('feat_v3','val25repro'),('feat','val25repro'),('feat_x','val25')):
|
| 4 |
+
A=pl.read_parquet(f'{V}/{d}/val25.parquet'); B=A.select('rowid').join(pl.read_parquet(f'{L}/{d}/{b}.parquet'),on='rowid',how='left')
|
| 5 |
+
full=json.load(open(f'{L}/repro_cmp.json'))[d]
|
| 6 |
+
import itertools
|
| 7 |
+
cols=[c for c in A.columns if c!='rowid' and c in B.columns and A[c].dtype.is_numeric()]
|
| 8 |
+
r={}
|
| 9 |
+
for c in cols:
|
| 10 |
+
x=A[c].cast(pl.Float64).to_numpy(); y=B[c].cast(pl.Float64).to_numpy(); m=np.isfinite(x)&np.isfinite(y)
|
| 11 |
+
if np.allclose(x[m],y[m],rtol=1e-4,atol=1e-5): continue
|
| 12 |
+
r[c]=dict(corr=round(float(np.corrcoef(x[m],y[m])[0,1]),5),frac_rows_diff=round(float((~np.isclose(x[m],y[m],rtol=1e-3,atol=1e-4)).mean()),4))
|
| 13 |
+
out[d]=dict(n_cols=len(cols),n_cols_diff=len(r),worst=sorted(r.items(),key=lambda kv:kv[1]['corr'])[:8])
|
| 14 |
+
print(json.dumps(out,indent=1)); json.dump(out,open(f'{L}/repro_cmp2.json','w'),indent=1)
|
parallel/lag/extra.py
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Extra leak-free features on v3 data (all stats from data with ts < C).
|
| 2 |
+
Usage: python extra.py NAME... (names: f1..f6, val25, val26, eval) -> v2work/feat_x/NAME.parquet
|
| 3 |
+
- con variants: k=32/128, recency-weighted (tau=2d) click/non-click centroids, item-side con (users who clicked i vs skipped i)
|
| 4 |
+
- user-cluster item CTR (kmeans on user SVD vectors, 64 & 256 clusters), smoothed toward item CTR
|
| 5 |
+
- within-event z-score and rank for each"""
|
| 6 |
+
import sys, os, time, numpy as np, polars as pl
|
| 7 |
+
from scipy import sparse
|
| 8 |
+
from sklearn.utils.extmath import randomized_svd
|
| 9 |
+
from sklearn.cluster import MiniBatchKMeans
|
| 10 |
+
sys.path.insert(0, '/workspace/huawei/v3')
|
| 11 |
+
from features import load, click_log
|
| 12 |
+
from split import J20, DAY, first_events
|
| 13 |
+
OUT = '/workspace/huawei/parallel/lag/feat_x'; os.makedirs(OUT, exist_ok=True)
|
| 14 |
+
TE, TR, EE, ER, NI, NU = load('/workspace/huawei/v3/data')
|
| 15 |
+
SPEC = {**{f'f{k}': (J20 + k * DAY, DAY) for k in range(1, 7)}, 'val25': (J20 + 5 * DAY, None), 'val26': (J20 + 6 * DAY, None), 'eval': (J20 + 7 * DAY, 'eval'),
|
| 16 |
+
**{f'e{k}': (J20 + k * DAY, (5 - k) * DAY) for k in range(1, 5)}, **{f'E{k}': (J20 + k * DAY, (7 - k) * DAY) for k in range(1, 7)}}
|
| 17 |
+
def nrm(M): return M / (np.linalg.norm(M, axis=1, keepdims=True) + 1e-9)
|
| 18 |
+
def csr(u, i, w): return sparse.csr_matrix((w.astype(np.float32), (u, i)), shape=(NU, NI))
|
| 19 |
+
def build(C, T_ev, T_rows):
|
| 20 |
+
P_ev = TE.filter(pl.col('ts') < C)
|
| 21 |
+
P_rows = TR.join(P_ev.select('eid', 'u', 'ts'), on='eid', how='inner')
|
| 22 |
+
CL = click_log(C, P_ev, P_rows, T_ev) # user-level: train data + each target row's own history
|
| 23 |
+
CLg = click_log(C, P_ev, P_rows, T_ev.head(0)) # global/item-level: train data only
|
| 24 |
+
assert CL['ts'].max() < C and P_rows['ts'].max() < C
|
| 25 |
+
R = T_rows.join(T_ev.select('eid', 'u'), on='eid', how='inner').select('rowid', 'eid', 'u', 'ii')
|
| 26 |
+
uu = R['u'].to_numpy().astype(np.int64); ii = R['ii'].to_numpy().astype(np.int64)
|
| 27 |
+
cg = CLg.group_by('u', 'ii').agg(pl.len().alias('n'))
|
| 28 |
+
Xc = csr(cg['u'].to_numpy(), cg['ii'].to_numpy(), np.ones(cg.height)); Scg = Xc.copy()
|
| 29 |
+
cnt = CL.group_by('u', 'ii').agg(pl.len().alias('n'), pl.col('ts').max().alias('lt'))
|
| 30 |
+
cu = cnt['u'].to_numpy(); ci = cnt['ii'].to_numpy()
|
| 31 |
+
df_ = np.asarray(Xc.sum(0)).ravel(); idf = np.log((NU + 1) / (df_ + 1)).astype(np.float32)
|
| 32 |
+
Xc = Xc @ sparse.diags(idf); Xc = sparse.diags(1 / (np.sqrt(np.asarray(Xc.multiply(Xc).sum(1)).ravel()) + 1e-6)) @ Xc
|
| 33 |
+
Xu = csr(cu, ci, np.ones(cnt.height)) @ sparse.diags(idf); Xu = sparse.diags(1 / (np.sqrt(np.asarray(Xu.multiply(Xu).sum(1)).ravel()) + 1e-6)) @ Xu
|
| 34 |
+
neg = P_rows.filter(pl.col('clicked') == 0)
|
| 35 |
+
nu_ = neg['u'].to_numpy(); ni_ = neg['ii'].to_numpy()
|
| 36 |
+
Sc = csr(cu, ci, np.ones(cnt.height)); Sn = csr(nu_, ni_, np.ones(neg.height))
|
| 37 |
+
rc = np.exp(-(C - cnt['lt'].to_numpy()) / (2 * DAY)); rn = np.exp(-(C - neg['ts'].to_numpy()) / (2 * DAY))
|
| 38 |
+
Scr = csr(cu, ci, rc); Snr = csr(nu_, ni_, rn)
|
| 39 |
+
F = {}
|
| 40 |
+
for k in (32, 64, 128):
|
| 41 |
+
U, S, Vt = randomized_svd(Xc, k, random_state=0, n_iter=5)
|
| 42 |
+
Vn = nrm(Vt.T * np.sqrt(S)); Un = nrm(np.asarray(Xu @ (Vt.T / np.sqrt(S)))); cand = Vn[ii] # user fold-in
|
| 43 |
+
nC = nrm(np.asarray(Sc @ Vn)); nN = nrm(np.asarray(Sn @ Vn))
|
| 44 |
+
F[f'x_con05_k{k}'] = (nrm(nC - 0.5 * nN)[uu] * cand).sum(1)
|
| 45 |
+
if k == 64:
|
| 46 |
+
nCr = nrm(np.asarray(Scr @ Vn)); nNr = nrm(np.asarray(Snr @ Vn))
|
| 47 |
+
for a, nm in ((0.5, '05'), (1.0, '1')):
|
| 48 |
+
F[f'x_conr{nm}_k64'] = (nrm(nCr - a * nNr)[uu] * cand).sum(1)
|
| 49 |
+
F['x_crcos_k64'] = (nCr[uu] * cand).sum(1)
|
| 50 |
+
# item side: centroid of users who clicked item minus users who skipped it, vs target user vector
|
| 51 |
+
iC = nrm(np.asarray(Scg.T @ Un)); iN = nrm(np.asarray(Sn.T @ Un)) # item side: train-only clicks
|
| 52 |
+
F['x_icon05_k64'] = (nrm(iC - 0.5 * iN)[ii] * Un[uu]).sum(1)
|
| 53 |
+
F['x_icos_k64'] = (iC[ii] * Un[uu]).sum(1)
|
| 54 |
+
# user-cluster CTR
|
| 55 |
+
act = np.unique(np.concatenate([P_rows['u'].to_numpy(), cg['u'].to_numpy()]))
|
| 56 |
+
for nc in (64, 256):
|
| 57 |
+
km = MiniBatchKMeans(nc, random_state=0, batch_size=4096, n_init=3).fit(Un[act])
|
| 58 |
+
lab = km.predict(Un).astype(np.int32)
|
| 59 |
+
g = float(P_rows['clicked'].mean())
|
| 60 |
+
pr = P_rows.select('ii', 'clicked', pl.Series('cl', lab[P_rows['u'].to_numpy()]))
|
| 61 |
+
ict = pr.group_by('ii').agg(((pl.col('clicked').sum() + 20 * g) / (pl.len() + 20)).alias('ictr'))
|
| 62 |
+
cct = pr.group_by('cl', 'ii').agg(pl.col('clicked').sum().alias('c'), pl.len().alias('n'))
|
| 63 |
+
q = pl.DataFrame({'cl': lab[uu], 'ii': ii.astype(np.int32), 'r': np.arange(len(uu))}).join(cct, on=['cl', 'ii'], how='left') \
|
| 64 |
+
.join(ict, on='ii', how='left').sort('r').with_columns(pl.col('ictr').fill_null(g), pl.col('c').fill_null(0), pl.col('n').fill_null(0))
|
| 65 |
+
F[f'x_clctr{nc}'] = ((q['c'] + 10 * q['ictr']) / (q['n'] + 10)).to_numpy()
|
| 66 |
+
F[f'x_clctr{nc}_lift'] = F[f'x_clctr{nc}'] / q['ictr'].to_numpy()
|
| 67 |
+
F[f'x_climp{nc}'] = q['n'].to_numpy().astype(np.float32)
|
| 68 |
+
R = R.with_columns([pl.Series(c, v.astype(np.float32)) for c, v in F.items()])
|
| 69 |
+
zc = [c for c in F if not c.startswith('x_climp')]
|
| 70 |
+
R = R.with_columns([((pl.col(c) - pl.col(c).mean().over('eid')) / (pl.col(c).std().over('eid') + 1e-6)).alias(c + '_z') for c in zc] +
|
| 71 |
+
[pl.col(c).rank('average').over('eid').alias(c + '_rk') for c in zc])
|
| 72 |
+
return R.drop('eid', 'u', 'ii')
|
| 73 |
+
for name in sys.argv[1:]:
|
| 74 |
+
t0 = time.time(); C, W = (J20 + 4 * DAY, 'lag') if name == 'val25lag' else SPEC[name]
|
| 75 |
+
if W == 'lag': T_ev = first_events(TE, J20 + 5 * DAY, None); R = build(C, T_ev, TR.join(T_ev.select('eid'), on='eid', how='semi'))
|
| 76 |
+
elif W == 'eval': R = build(C, EE, ER)
|
| 77 |
+
else:
|
| 78 |
+
T_ev = first_events(TE, C, W); R = build(C, T_ev, TR.join(T_ev.select('eid'), on='eid', how='semi'))
|
| 79 |
+
R.write_parquet(f'{OUT}/{name}.parquet'); print(name, R.shape, f'{time.time()-t0:.0f}s', flush=True)
|
parallel/lag/extra2.py
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Extra features #2 (leak-free; everything from ts < C):
|
| 2 |
+
- ridge 'taste vector' per user on frozen SVD k64 item vectors (clicks y=1 incl. pre-C click log, skipped impressions y=0 or -0.5)
|
| 3 |
+
- user state: time since last event/click, event-level click rate, recent-event click streaks, events in last 24h before C
|
| 4 |
+
Usage: python extra2.py NAME... -> v2work/feat_x2/NAME.parquet"""
|
| 5 |
+
import sys, os, time, numpy as np, polars as pl
|
| 6 |
+
from scipy import sparse
|
| 7 |
+
from sklearn.utils.extmath import randomized_svd
|
| 8 |
+
sys.path.insert(0, '/workspace/huawei/v3')
|
| 9 |
+
from features import load, click_log
|
| 10 |
+
from split import J20, DAY, first_events
|
| 11 |
+
OUT = '/workspace/huawei/parallel/lag/feat_x2'; os.makedirs(OUT, exist_ok=True)
|
| 12 |
+
TE, TR, EE, ER, NI, NU = load('/workspace/huawei/v3/data')
|
| 13 |
+
SPEC = {**{f'f{k}': (J20 + k * DAY, DAY) for k in range(1, 7)}, 'val25': (J20 + 5 * DAY, None), 'val26': (J20 + 6 * DAY, None),
|
| 14 |
+
'val22': (J20 + 2 * DAY, None), 'eval': (J20 + 7 * DAY, 'eval'),
|
| 15 |
+
**{f'e{k}': (J20 + k * DAY, (5 - k) * DAY) for k in range(1, 5)}, **{f'E{k}': (J20 + k * DAY, (7 - k) * DAY) for k in range(1, 7)}}
|
| 16 |
+
def nrm(M): return M / (np.linalg.norm(M, axis=1, keepdims=True) + 1e-9)
|
| 17 |
+
def ridge_users(Wm, Ym, V, lam):
|
| 18 |
+
"""per-user solve (V_I^T W V_I + lam I) w = V_I^T (W*y). Wm, Ym: csr users x items (weights, weight*target)."""
|
| 19 |
+
k = V.shape[1]; A = np.zeros((Wm.shape[0], k, k), np.float32)
|
| 20 |
+
for a in range(k):
|
| 21 |
+
cols = Wm @ (V * V[:, a:a + 1]) # (NU, k): sum_i w_ui v_ia v_ib
|
| 22 |
+
A[:, a, :] = cols
|
| 23 |
+
A += lam * np.eye(k, dtype=np.float32)[None]
|
| 24 |
+
b = np.asarray(Ym @ V, dtype=np.float32)
|
| 25 |
+
return np.linalg.solve(A, b[:, :, None])[:, :, 0]
|
| 26 |
+
def build(C, T_ev, T_rows):
|
| 27 |
+
P_ev = TE.filter(pl.col('ts') < C)
|
| 28 |
+
P_rows = TR.join(P_ev.select('eid', 'u', 'ts'), on='eid', how='inner')
|
| 29 |
+
CL = click_log(C, P_ev, P_rows, T_ev) # user-level: train data + each target row's own history
|
| 30 |
+
CLg = click_log(C, P_ev, P_rows, T_ev.head(0)) # global/item-level: train data only
|
| 31 |
+
assert CL['ts'].max() < C and P_rows['ts'].max() < C
|
| 32 |
+
R = T_rows.join(T_ev.select('eid', 'u', 'ts'), on='eid', how='inner').select('rowid', 'eid', 'u', 'ii', 'ts')
|
| 33 |
+
uu = R['u'].to_numpy().astype(np.int64); ii = R['ii'].to_numpy().astype(np.int64)
|
| 34 |
+
# SVD exactly as v3 (tf-idf, row-normalised user x item click matrix)
|
| 35 |
+
cnt = CLg.group_by('u', 'ii').agg(pl.len().alias('n'))
|
| 36 |
+
Xc = sparse.csr_matrix((np.ones(cnt.height, np.float32), (cnt['u'].to_numpy(), cnt['ii'].to_numpy())), shape=(NU, NI))
|
| 37 |
+
df_ = np.asarray(Xc.sum(0)).ravel(); idf = np.log((NU + 1) / (df_ + 1)).astype(np.float32)
|
| 38 |
+
Xc = Xc @ sparse.diags(idf); Xc = sparse.diags(1 / (np.sqrt(np.asarray(Xc.multiply(Xc).sum(1)).ravel()) + 1e-6)) @ Xc
|
| 39 |
+
U, S, Vt = randomized_svd(Xc, 64, random_state=0, n_iter=5); Vn = nrm(Vt.T * np.sqrt(S)).astype(np.float32)
|
| 40 |
+
# (user,item) labels: clicked anywhere before C -> 1 ; shown-but-never-clicked -> 0
|
| 41 |
+
pos = CL.select('u', 'ii').unique().with_columns(pl.lit(1.0).alias('y'))
|
| 42 |
+
neg = P_rows.filter(pl.col('clicked') == 0).select('u', 'ii').unique().join(pos.select('u', 'ii'), on=['u', 'ii'], how='anti').with_columns(pl.lit(0.0).alias('y'))
|
| 43 |
+
L = pl.concat([pos, neg])
|
| 44 |
+
lu = L['u'].to_numpy(); li = L['ii'].to_numpy(); ly = L['y'].to_numpy().astype(np.float32)
|
| 45 |
+
Wm = sparse.csr_matrix((np.ones(len(ly), np.float32), (lu, li)), shape=(NU, NI))
|
| 46 |
+
cand = Vn[ii]; F = {}
|
| 47 |
+
for lam, yneg, nm in ((1.0, 0.0, 'l1'), (10.0, 0.0, 'l10'), (3.0, -0.5, 'l3n')):
|
| 48 |
+
yy = np.where(ly > 0, 1.0, yneg).astype(np.float32)
|
| 49 |
+
Ym = sparse.csr_matrix((yy, (lu, li)), shape=(NU, NI))
|
| 50 |
+
Wu = ridge_users(Wm, Ym, Vn, lam)
|
| 51 |
+
F[f'r_{nm}'] = (Wu[uu] * cand).sum(1)
|
| 52 |
+
# user state features
|
| 53 |
+
ue = P_ev.select('eid', 'u', 'ts').join(P_rows.group_by('eid').agg(pl.col('clicked').sum().alias('nc')), on='eid', how='left') \
|
| 54 |
+
.with_columns(pl.col('nc').fill_null(0)).sort('u', 'ts')
|
| 55 |
+
us = ue.group_by('u').agg(pl.col('ts').last().alias('last_ev'), (pl.col('nc') > 0).mean().alias('s_evrate'), pl.len().alias('s_nev'),
|
| 56 |
+
pl.col('nc').tail(1).sum().alias('s_clk_last1'), pl.col('nc').tail(3).sum().alias('s_clk_last3'),
|
| 57 |
+
(pl.col('nc').tail(5) > 0).sum().alias('s_evclk_last5'), (pl.col('ts') >= C - DAY).sum().alias('s_nev24h'),
|
| 58 |
+
pl.col('ts').filter(pl.col('nc') > 0).max().alias('last_clkev'))
|
| 59 |
+
lc = CL.group_by('u').agg(pl.col('ts').max().alias('last_click'), (pl.col('ts') >= C - DAY).sum().alias('s_clk24h'))
|
| 60 |
+
E = T_ev.select('eid', 'u', 'ts').join(us, on='u', how='left').join(lc, on='u', how='left').with_columns(
|
| 61 |
+
((pl.col('ts') - pl.col('last_ev')) / 3600).alias('s_since_ev_h'), ((pl.col('ts') - pl.col('last_click')) / 3600).alias('s_since_clk_h'),
|
| 62 |
+
((pl.col('ts') - pl.col('last_clkev')) / 3600).alias('s_since_clkev_h')).drop('u', 'ts', 'last_ev', 'last_click', 'last_clkev')
|
| 63 |
+
R = R.with_columns([pl.Series(c, v.astype(np.float32)) for c, v in F.items()])
|
| 64 |
+
R = R.with_columns([((pl.col(c) - pl.col(c).mean().over('eid')) / (pl.col(c).std().over('eid') + 1e-6)).alias(c + '_z') for c in F] +
|
| 65 |
+
[(pl.col(c) - pl.col(c).max().over('eid')).alias(c + '_dmax') for c in F])
|
| 66 |
+
R = R.join(E, on='eid', how='left')
|
| 67 |
+
return R.drop('eid', 'u', 'ii', 'ts')
|
| 68 |
+
for name in sys.argv[1:]:
|
| 69 |
+
t0 = time.time(); C, W = (J20 + 4 * DAY, 'lag') if name == 'val25lag' else SPEC[name]
|
| 70 |
+
if W == 'lag': T_ev = first_events(TE, J20 + 5 * DAY, None); R = build(C, T_ev, TR.join(T_ev.select('eid'), on='eid', how='semi'))
|
| 71 |
+
elif W == 'eval': R = build(C, EE, ER)
|
| 72 |
+
else:
|
| 73 |
+
T_ev = first_events(TE, C, W); R = build(C, T_ev, TR.join(T_ev.select('eid'), on='eid', how='semi'))
|
| 74 |
+
R.write_parquet(f'{OUT}/{name}.parquet'); print(name, R.shape, f'{time.time()-t0:.0f}s', flush=True)
|
parallel/lag/lag_buckets.json
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
{"val25": {"auc": 0.77748, "le1d": 0.78184, "d1_2": 0.75895, "event": 0.65358}, "val25lag": {"auc": 0.7647, "le1d": 0.76946, "d1_2": 0.7443, "event": 0.64837}}
|
parallel/lag/lagtest.json
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"n_feats": 325,
|
| 3 |
+
"val25": {
|
| 4 |
+
"200": 0.77684,
|
| 5 |
+
"300": 0.77724,
|
| 6 |
+
"400": 0.77748
|
| 7 |
+
},
|
| 8 |
+
"val25lag": {
|
| 9 |
+
"200": 0.76434,
|
| 10 |
+
"300": 0.76472,
|
| 11 |
+
"400": 0.7647
|
| 12 |
+
}
|
| 13 |
+
}
|
parallel/lag/lagtest.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Train v2work's combo config (notime, XF=1, XF2=1, f1..f4, PARAMS from snapshot combo.py, 400 rounds, 3 threads) on v2work's
|
| 2 |
+
on-disk tables (read-only). Then predict (a) val25 as built and (b) val25lag (the same target rows, all stats frozen at Jan24).
|
| 3 |
+
Preds are saved in parallel/lag/."""
|
| 4 |
+
import sys, os, json, gc, numpy as np, polars as pl, lightgbm as lgb
|
| 5 |
+
sys.path.insert(0,'/workspace/huawei/parallel/src_snapshot/v2work'); sys.path.insert(1,'/workspace/huawei/v3')
|
| 6 |
+
from feats import DROP
|
| 7 |
+
from features import feature_cols, GROUPS
|
| 8 |
+
from sklearn.metrics import roc_auc_score
|
| 9 |
+
V='/workspace/huawei/v2work'; L='/workspace/huawei/parallel/lag'
|
| 10 |
+
V2_OF={'f1':'h1','f2':'h2','f3':'h3','f4':'h4','val25':'val25','val25lag':'val25lag'}
|
| 11 |
+
TIME=['tm_stale','tm_hour','tm_tb','tm_dt','v2_tb','v2_dt','v2_stale','v2_hour']
|
| 12 |
+
def load(n, base):
|
| 13 |
+
a=pl.read_parquet(f'{base}/feat_v3/{n}.parquet'); b=pl.read_parquet(f'{base}/feat/{V2_OF[n]}.parquet')
|
| 14 |
+
f2=[c for c in b.columns if c not in DROP]; b=b.select(['rowid']+f2).rename({c:'v2_'+c for c in f2})
|
| 15 |
+
a=a.join(b,on='rowid',how='left')
|
| 16 |
+
a=a.join(pl.read_parquet(f'{base}/feat_x/{n}.parquet'),on='rowid',how='left')
|
| 17 |
+
return a.join(pl.read_parquet(f'{base}/feat_x2/{n}.parquet'),on='rowid',how='left')
|
| 18 |
+
PARAMS=dict(objective='binary',learning_rate=0.05,num_leaves=63,min_data_in_leaf=200,feature_fraction=0.7,bagging_fraction=0.8,
|
| 19 |
+
bagging_freq=1,lambda_l2=10.0,verbose=-1,seed=42,num_threads=3,deterministic=True)
|
| 20 |
+
feats=None; Xs=[]; ys=[]
|
| 21 |
+
for k in range(1,5):
|
| 22 |
+
t=load(f'f{k}',V)
|
| 23 |
+
if feats is None:
|
| 24 |
+
feats=[c for c in feature_cols(t,GROUPS)+[c for c in t.columns if c.startswith(('v2_','x_','r_','s_'))] if c not in TIME]
|
| 25 |
+
Xs.append(t.select(feats).to_numpy().astype(np.float32)); ys.append(t['clicked'].to_numpy()); del t; gc.collect()
|
| 26 |
+
X=np.concatenate(Xs); del Xs; gc.collect()
|
| 27 |
+
print('train',X.shape,flush=True)
|
| 28 |
+
ds=lgb.Dataset(X,np.concatenate(ys),feature_name=feats,free_raw_data=True).construct(); del X; gc.collect()
|
| 29 |
+
m=lgb.train(PARAMS,ds,num_boost_round=400); del ds; gc.collect(); m.save_model(f'{L}/model_f1to4.txt')
|
| 30 |
+
res={'n_feats':len(feats)}
|
| 31 |
+
for n,base in (('val25',V),('val25lag',L)):
|
| 32 |
+
va=load(n,base); miss=[c for c in feats if c not in va.columns]; assert not miss, miss
|
| 33 |
+
Xv=va.select(feats).to_numpy().astype(np.float32); y=va['clicked'].to_numpy()
|
| 34 |
+
res[n]={k:round(roc_auc_score(y,m.predict(Xv,num_iteration=k)),5) for k in (200,300,400)}
|
| 35 |
+
pl.DataFrame({'rowid':va['rowid'],'p':m.predict(Xv,num_iteration=400)}).write_parquet(f'{L}/pred_{n}.parquet')
|
| 36 |
+
print(n,res[n],flush=True)
|
| 37 |
+
json.dump(res,open(f'{L}/lagtest.json','w'),indent=1)
|
parallel/lag/r_v2.py
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import sys; sys.path.insert(0,'/workspace/huawei/parallel/src_snapshot/v2work')
|
| 2 |
+
from feats import load, first_events, build, log
|
| 3 |
+
D='/workspace/huawei/v2work'; DAY=86400; J20=1737331200
|
| 4 |
+
TE,TR,EE,ER,NI=load(D)
|
| 5 |
+
T_ev=first_events(TE,J20+5*DAY,2*DAY); T_rows=TR.join(T_ev.select('eid'),on='eid')
|
| 6 |
+
R=build(J20+5*DAY,T_ev,T_rows,TE,TR,NI)
|
| 7 |
+
R.drop('h','h_ts').write_parquet('/workspace/huawei/parallel/lag/feat/val25repro.parquet'); log('wrote',R.shape)
|
parallel/lag/r_v3.py
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""lag-1d: val25 targets (first event/user >= Jan25) with ALL stats frozen at Jan24 (v3feat_ne.build from snapshot)."""
|
| 2 |
+
import sys; sys.path.insert(0,'/workspace/huawei/parallel/src_snapshot/v2work'); sys.path.insert(1,'/workspace/huawei/v3')
|
| 3 |
+
from v3feat_ne import load, build, log
|
| 4 |
+
from split import J20, DAY, first_events
|
| 5 |
+
TE, TR, EE, ER, NI, NU = load('/workspace/huawei/v3/data')
|
| 6 |
+
T_ev = first_events(TE, J20 + 5 * DAY)
|
| 7 |
+
R = build(J20 + 5 * DAY, T_ev, TR.join(T_ev.select('eid'), on='eid', how='semi'), TE, TR, NI, NU)
|
| 8 |
+
R.write_parquet('/workspace/huawei/parallel/lag/feat_v3/val25repro.parquet'); log('wrote', R.shape)
|
parallel/lag/repro_cmp.json
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"feat_v3": {
|
| 3 |
+
"ncols": 169,
|
| 4 |
+
"only_disk": [],
|
| 5 |
+
"n_diff": 35,
|
| 6 |
+
"diff": {
|
| 7 |
+
"co_cos_max20": [
|
| 8 |
+
0.07715167850255966,
|
| 9 |
+
0
|
| 10 |
+
],
|
| 11 |
+
"co_cos_mean20": [
|
| 12 |
+
0.018184823915362358,
|
| 13 |
+
0
|
| 14 |
+
],
|
| 15 |
+
"co_cos_max5": [
|
| 16 |
+
0.0690065547823906,
|
| 17 |
+
0
|
| 18 |
+
],
|
| 19 |
+
"co_cos_mean5": [
|
| 20 |
+
0.027216553688049316,
|
| 21 |
+
0
|
| 22 |
+
],
|
| 23 |
+
"co_cos_last": [
|
| 24 |
+
0.06804138422012329,
|
| 25 |
+
0
|
| 26 |
+
],
|
| 27 |
+
"co_tr_max20": [
|
| 28 |
+
0.0025684931315481663,
|
| 29 |
+
0
|
| 30 |
+
],
|
| 31 |
+
"co_tr_mean20": [
|
| 32 |
+
0.0021482277661561966,
|
| 33 |
+
0
|
| 34 |
+
],
|
| 35 |
+
"co_tr_max5": [
|
| 36 |
+
0.0025641026441007853,
|
| 37 |
+
0
|
| 38 |
+
],
|
| 39 |
+
"co_tr_mean5": [
|
| 40 |
+
0.0021482277661561966,
|
| 41 |
+
0
|
| 42 |
+
],
|
| 43 |
+
"co_tr_last": [
|
| 44 |
+
0.002553191501647234,
|
| 45 |
+
0
|
| 46 |
+
],
|
| 47 |
+
"co_cos_rw": [
|
| 48 |
+
0.12024806439876556,
|
| 49 |
+
0
|
| 50 |
+
],
|
| 51 |
+
"co_cos_sum20": [
|
| 52 |
+
0.279508501291275,
|
| 53 |
+
0
|
| 54 |
+
]
|
| 55 |
+
}
|
| 56 |
+
},
|
| 57 |
+
"feat": {
|
| 58 |
+
"ncols": 122,
|
| 59 |
+
"only_disk": [],
|
| 60 |
+
"n_diff": 14,
|
| 61 |
+
"diff": {
|
| 62 |
+
"tr_last": [
|
| 63 |
+
0.5,
|
| 64 |
+
0
|
| 65 |
+
],
|
| 66 |
+
"trp_last": [
|
| 67 |
+
0.011152416467666626,
|
| 68 |
+
0
|
| 69 |
+
],
|
| 70 |
+
"tr_last3": [
|
| 71 |
+
1.0,
|
| 72 |
+
0
|
| 73 |
+
],
|
| 74 |
+
"trp_last3": [
|
| 75 |
+
0.029003147035837173,
|
| 76 |
+
0
|
| 77 |
+
],
|
| 78 |
+
"svd_cos_rk": [
|
| 79 |
+
1.0,
|
| 80 |
+
0
|
| 81 |
+
],
|
| 82 |
+
"svd_ucos_rk": [
|
| 83 |
+
1.0,
|
| 84 |
+
0
|
| 85 |
+
],
|
| 86 |
+
"svd32_cos_rk": [
|
| 87 |
+
1.0,
|
| 88 |
+
0
|
| 89 |
+
],
|
| 90 |
+
"svd16_cos_rk": [
|
| 91 |
+
1.0,
|
| 92 |
+
0
|
| 93 |
+
],
|
| 94 |
+
"svd24_cos_rk": [
|
| 95 |
+
1.0,
|
| 96 |
+
0
|
| 97 |
+
],
|
| 98 |
+
"svd48_cos_rk": [
|
| 99 |
+
1.0,
|
| 100 |
+
0
|
| 101 |
+
],
|
| 102 |
+
"svd_maxsim_rk": [
|
| 103 |
+
1.0,
|
| 104 |
+
0
|
| 105 |
+
],
|
| 106 |
+
"svd_top3_rk": [
|
| 107 |
+
1.0,
|
| 108 |
+
0
|
| 109 |
+
]
|
| 110 |
+
}
|
| 111 |
+
},
|
| 112 |
+
"feat_x": {
|
| 113 |
+
"ncols": 38,
|
| 114 |
+
"only_disk": [],
|
| 115 |
+
"n_diff": 24,
|
| 116 |
+
"diff": {
|
| 117 |
+
"x_clctr64": [
|
| 118 |
+
0.15912476181983948,
|
| 119 |
+
0
|
| 120 |
+
],
|
| 121 |
+
"x_clctr64_lift": [
|
| 122 |
+
2.1029531955718994,
|
| 123 |
+
0
|
| 124 |
+
],
|
| 125 |
+
"x_climp64": [
|
| 126 |
+
12.0,
|
| 127 |
+
0
|
| 128 |
+
],
|
| 129 |
+
"x_clctr256": [
|
| 130 |
+
0.24428869783878326,
|
| 131 |
+
0
|
| 132 |
+
],
|
| 133 |
+
"x_clctr256_lift": [
|
| 134 |
+
4.538407802581787,
|
| 135 |
+
0
|
| 136 |
+
],
|
| 137 |
+
"x_climp256": [
|
| 138 |
+
17.0,
|
| 139 |
+
0
|
| 140 |
+
],
|
| 141 |
+
"x_con05_k128": [
|
| 142 |
+
0.009315282106399536,
|
| 143 |
+
0
|
| 144 |
+
],
|
| 145 |
+
"x_clctr64_z": [
|
| 146 |
+
2.288796730339527,
|
| 147 |
+
0
|
| 148 |
+
],
|
| 149 |
+
"x_clctr64_lift_z": [
|
| 150 |
+
2.0558256804943085,
|
| 151 |
+
0
|
| 152 |
+
],
|
| 153 |
+
"x_clctr256_z": [
|
| 154 |
+
3.432311713695526,
|
| 155 |
+
0
|
| 156 |
+
],
|
| 157 |
+
"x_clctr256_lift_z": [
|
| 158 |
+
5.164675951004028,
|
| 159 |
+
0
|
| 160 |
+
],
|
| 161 |
+
"x_con05_k128_z": [
|
| 162 |
+
0.08607450127601624,
|
| 163 |
+
0
|
| 164 |
+
]
|
| 165 |
+
}
|
| 166 |
+
},
|
| 167 |
+
"feat_x2": {
|
| 168 |
+
"ncols": 19,
|
| 169 |
+
"only_disk": [],
|
| 170 |
+
"n_diff": 0,
|
| 171 |
+
"diff": {}
|
| 172 |
+
}
|
| 173 |
+
}
|
parallel/lag/repro_cmp2.json
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"feat_v3": {
|
| 3 |
+
"n_cols": 169,
|
| 4 |
+
"n_cols_diff": 60,
|
| 5 |
+
"worst": [
|
| 6 |
+
[
|
| 7 |
+
"ev_co_tr_last_rk",
|
| 8 |
+
{
|
| 9 |
+
"corr": 0.98861,
|
| 10 |
+
"frac_rows_diff": 0.0011
|
| 11 |
+
}
|
| 12 |
+
],
|
| 13 |
+
[
|
| 14 |
+
"ev_co_tr_last_z",
|
| 15 |
+
{
|
| 16 |
+
"corr": 0.98947,
|
| 17 |
+
"frac_rows_diff": 0.0011
|
| 18 |
+
}
|
| 19 |
+
],
|
| 20 |
+
[
|
| 21 |
+
"ev_co_cos_rw_rk",
|
| 22 |
+
{
|
| 23 |
+
"corr": 0.98987,
|
| 24 |
+
"frac_rows_diff": 0.0137
|
| 25 |
+
}
|
| 26 |
+
],
|
| 27 |
+
[
|
| 28 |
+
"ev_co_cos_max20_rk",
|
| 29 |
+
{
|
| 30 |
+
"corr": 0.99028,
|
| 31 |
+
"frac_rows_diff": 0.0135
|
| 32 |
+
}
|
| 33 |
+
],
|
| 34 |
+
[
|
| 35 |
+
"ev_co_cos_rw_z",
|
| 36 |
+
{
|
| 37 |
+
"corr": 0.99066,
|
| 38 |
+
"frac_rows_diff": 0.0254
|
| 39 |
+
}
|
| 40 |
+
],
|
| 41 |
+
[
|
| 42 |
+
"ev_co_cos_max20_z",
|
| 43 |
+
{
|
| 44 |
+
"corr": 0.99071,
|
| 45 |
+
"frac_rows_diff": 0.0146
|
| 46 |
+
}
|
| 47 |
+
],
|
| 48 |
+
[
|
| 49 |
+
"ev_co_tr_max5_rk",
|
| 50 |
+
{
|
| 51 |
+
"corr": 0.99311,
|
| 52 |
+
"frac_rows_diff": 0.0043
|
| 53 |
+
}
|
| 54 |
+
],
|
| 55 |
+
[
|
| 56 |
+
"ev_co_tr_max5_z",
|
| 57 |
+
{
|
| 58 |
+
"corr": 0.99427,
|
| 59 |
+
"frac_rows_diff": 0.0045
|
| 60 |
+
}
|
| 61 |
+
]
|
| 62 |
+
]
|
| 63 |
+
},
|
| 64 |
+
"feat": {
|
| 65 |
+
"n_cols": 122,
|
| 66 |
+
"n_cols_diff": 31,
|
| 67 |
+
"worst": [
|
| 68 |
+
[
|
| 69 |
+
"tr_last3_rk",
|
| 70 |
+
{
|
| 71 |
+
"corr": 0.99975,
|
| 72 |
+
"frac_rows_diff": 0.0001
|
| 73 |
+
}
|
| 74 |
+
],
|
| 75 |
+
[
|
| 76 |
+
"trp_last3",
|
| 77 |
+
{
|
| 78 |
+
"corr": 0.99995,
|
| 79 |
+
"frac_rows_diff": 0.0001
|
| 80 |
+
}
|
| 81 |
+
],
|
| 82 |
+
[
|
| 83 |
+
"tr_last3",
|
| 84 |
+
{
|
| 85 |
+
"corr": 0.99996,
|
| 86 |
+
"frac_rows_diff": 0.0001
|
| 87 |
+
}
|
| 88 |
+
],
|
| 89 |
+
[
|
| 90 |
+
"tr_last3_dm",
|
| 91 |
+
{
|
| 92 |
+
"corr": 0.99996,
|
| 93 |
+
"frac_rows_diff": 0.0006
|
| 94 |
+
}
|
| 95 |
+
],
|
| 96 |
+
[
|
| 97 |
+
"tr_last",
|
| 98 |
+
{
|
| 99 |
+
"corr": 0.99997,
|
| 100 |
+
"frac_rows_diff": 0.0
|
| 101 |
+
}
|
| 102 |
+
],
|
| 103 |
+
[
|
| 104 |
+
"trp_last",
|
| 105 |
+
{
|
| 106 |
+
"corr": 0.99997,
|
| 107 |
+
"frac_rows_diff": 0.0
|
| 108 |
+
}
|
| 109 |
+
],
|
| 110 |
+
[
|
| 111 |
+
"svd_cos",
|
| 112 |
+
{
|
| 113 |
+
"corr": 1.0,
|
| 114 |
+
"frac_rows_diff": 0.0
|
| 115 |
+
}
|
| 116 |
+
],
|
| 117 |
+
[
|
| 118 |
+
"svd_ucos",
|
| 119 |
+
{
|
| 120 |
+
"corr": 1.0,
|
| 121 |
+
"frac_rows_diff": 0.0
|
| 122 |
+
}
|
| 123 |
+
]
|
| 124 |
+
]
|
| 125 |
+
},
|
| 126 |
+
"feat_x": {
|
| 127 |
+
"n_cols": 38,
|
| 128 |
+
"n_cols_diff": 38,
|
| 129 |
+
"worst": [
|
| 130 |
+
[
|
| 131 |
+
"x_clctr256_lift_rk",
|
| 132 |
+
{
|
| 133 |
+
"corr": 0.99607,
|
| 134 |
+
"frac_rows_diff": 0.0176
|
| 135 |
+
}
|
| 136 |
+
],
|
| 137 |
+
[
|
| 138 |
+
"x_clctr256_lift_z",
|
| 139 |
+
{
|
| 140 |
+
"corr": 0.99638,
|
| 141 |
+
"frac_rows_diff": 0.0498
|
| 142 |
+
}
|
| 143 |
+
],
|
| 144 |
+
[
|
| 145 |
+
"x_clctr256_lift",
|
| 146 |
+
{
|
| 147 |
+
"corr": 0.9974,
|
| 148 |
+
"frac_rows_diff": 0.0069
|
| 149 |
+
}
|
| 150 |
+
],
|
| 151 |
+
[
|
| 152 |
+
"x_clctr256",
|
| 153 |
+
{
|
| 154 |
+
"corr": 0.99858,
|
| 155 |
+
"frac_rows_diff": 0.0069
|
| 156 |
+
}
|
| 157 |
+
],
|
| 158 |
+
[
|
| 159 |
+
"x_clctr256_z",
|
| 160 |
+
{
|
| 161 |
+
"corr": 0.99866,
|
| 162 |
+
"frac_rows_diff": 0.0496
|
| 163 |
+
}
|
| 164 |
+
],
|
| 165 |
+
[
|
| 166 |
+
"x_climp256",
|
| 167 |
+
{
|
| 168 |
+
"corr": 0.99869,
|
| 169 |
+
"frac_rows_diff": 0.0068
|
| 170 |
+
}
|
| 171 |
+
],
|
| 172 |
+
[
|
| 173 |
+
"x_clctr256_rk",
|
| 174 |
+
{
|
| 175 |
+
"corr": 0.99881,
|
| 176 |
+
"frac_rows_diff": 0.0062
|
| 177 |
+
}
|
| 178 |
+
],
|
| 179 |
+
[
|
| 180 |
+
"x_clctr64_lift_z",
|
| 181 |
+
{
|
| 182 |
+
"corr": 0.99995,
|
| 183 |
+
"frac_rows_diff": 0.0032
|
| 184 |
+
}
|
| 185 |
+
]
|
| 186 |
+
]
|
| 187 |
+
}
|
| 188 |
+
}
|
parallel/lag/run2.log
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
12:46:07 build C=1737763200 rows=439640 cols=170 45s
|
| 2 |
+
12:46:07 wrote (439640, 170)
|
| 3 |
+
12:46:13 pool 886177
|
| 4 |
+
12:46:18 cooc pairs 18012356
|
| 5 |
+
12:46:21 svd done
|
| 6 |
+
12:46:48 build C=1737763200 rows=439640 36s
|
| 7 |
+
12:46:48 wrote (439640, 125)
|
| 8 |
+
val25 (439640, 39) 19s
|
| 9 |
+
val25 (439640, 20) 41s
|
| 10 |
+
train (1402200, 325)
|
| 11 |
+
val25 {200: 0.77684, 300: 0.77724, 400: 0.77748}
|
| 12 |
+
val25lag {200: 0.76434, 300: 0.76472, 400: 0.7647}
|
parallel/report.md
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# v2work vs v3: leakage audit and like-for-like comparison (parallel check)
|
| 2 |
+
Started 23 Oct 2026 12:35 (Europe/Dublin). v2work and v3 were only read. Everything here lives in /workspace/huawei/parallel/.
|
| 3 |
+
The source snapshot comes from v2work/out/solution_v3_src.zip (06:24) and is unzipped in parallel/src_snapshot/.
|
| 4 |
+
Current v2work code differs from it only by the added `d{k}` fold specs (12:27 edits), so the snapshot is what produced submission_v3.
|
| 5 |
+
|
| 6 |
+
## Status log
|
| 7 |
+
- 12:45 Audit of the code path done (see below). Like-for-like scoring done (score_val.json). Submission check done (subcheck.json).
|
| 8 |
+
|
| 9 |
+
## 1. Leakage audit of the v2work "ne" (no-eval) path
|
| 10 |
+
Citations point to parallel/src_snapshot/v2work/* (identical to what produced out/submission_v3.csv).
|
| 11 |
+
C is the cutoff. "Own history" is the history list that is a given input field on every row, including eval rows.
|
| 12 |
+
|
| 13 |
+
**a. Are training rows only events before the cutoff? YES.**
|
| 14 |
+
- v3fold.py L11–12 SPEC: f_k = first event per user in [J20+k·d, +1d).
|
| 15 |
+
- In val mode, combo.py L44 trains on f1..f4 only (cut25). All target ts < Jan25.
|
| 16 |
+
- Val25 is the first event per user at or after Jan25 (W=None), the same as our v3/split.
|
| 17 |
+
- Final (combo.py L68, FOLDS unset in solution_v3.py) uses f1..f6, whose targets fall in Jan21..Jan26 (<Jan27).
|
| 18 |
+
- build_folds.py L9–12: val25 is (Jan25, 2d), and h1..h4 are 1-day windows.
|
| 19 |
+
|
| 20 |
+
**b. Are label-derived features computed only from labels before the cutoff? YES.**
|
| 21 |
+
- v3feat_ne.py L109–110: P_ev and P_rows are filtered to ts<C. All CTR, impression and click stats come from these.
|
| 22 |
+
- feats.py L40–41: same filter.
|
| 23 |
+
- extra.py L22–24 and extra2.py L29–31 assert `CL.ts.max()<C and P_rows.ts.max()<C`.
|
| 24 |
+
- Cluster CTR (extra.py L60) and user state (extra2.py L53–59) come from P_rows and P_ev.
|
| 25 |
+
|
| 26 |
+
**c. Does any feature use the unlabelled eval rows? Only their own history, and only for user-level features.**
|
| 27 |
+
- Item, global, co-occurrence and SVD stats come from CLg = click_log(..., T_ev.head(0)), which is train only:
|
| 28 |
+
- v3feat_ne.py L113–114 (CL vs CLg)
|
| 29 |
+
- L128 (item ip)
|
| 30 |
+
- L173 (cooc)
|
| 31 |
+
- L195 (SVD from CLg)
|
| 32 |
+
- feats.py L54–55 poolg → L65, L87, L96
|
| 33 |
+
- extra.py L31 (idf from CLg)
|
| 34 |
+
- extra2.py L35–39 (SVD)
|
| 35 |
+
- User-level stats use each row's own pre-C history:
|
| 36 |
+
- v3feat_ne.py L209–212 and L222 (user fold-in onto train-only item factors)
|
| 37 |
+
- feats.py L49–50 and L117–122
|
| 38 |
+
- extra.py L36
|
| 39 |
+
- extra2.py L41
|
| 40 |
+
- This is standard: every eval row carries its own history. It doesn't pool across eval rows. No eval labels exist or are used.
|
| 41 |
+
- Two minor transductive touches, label-free with pre-C clicks only:
|
| 42 |
+
- extra.py L51 builds item-side vectors iC = Scg.T @ Un. Scg is train-only clicks, but Un includes the target users' folded-in vectors.
|
| 43 |
+
- extra.py L55–57 fits KMeans on Un over active users, which can include target users.
|
| 44 |
+
- Neither uses labels or post-C data. Expected effect is small.
|
| 45 |
+
- write_sub.py reads evaluation.csv only for rowid order and validation.
|
| 46 |
+
|
| 47 |
+
**d. Is the validation set the same as ours? YES.**
|
| 48 |
+
- val25: 439,640 rows, rowid sets identical, 0 label mismatches.
|
| 49 |
+
- val26: 351,380 rows, identical (score_val.json).
|
| 50 |
+
|
| 51 |
+
**Does submission_v3.csv come from the ne path? YES.**
|
| 52 |
+
- It equals v2work/eval_pred_v3.parquet (max abs diff 5e-9) and differs from the old eval_pred.parquet by up to 0.34.
|
| 53 |
+
- eval_pred_v3 was written by `combo.py final 400 3 notime` with XF=1 XF2=1 (ne_final_v3.log: 2,105,880 rows = f1..f6, 325 feats, 3 seeds).
|
| 54 |
+
- All input tables (feat_v3 at 04:06–04:24, feat at 04:06–04:12, feat_x at 04:06–04:15, feat_x2 at 04:16–04:27) were written by the ne_* runs after the no-eval edits (feats.py and v3feat_ne.py at 04:03).
|
| 55 |
+
|
| 56 |
+
**FLAG: our own v3 is less strict than v2work here.**
|
| 57 |
+
- v3/features.py builds item stats (L127), co-occurrence (L172) and the SVD matrix (L194) from CL, which includes every target row's own history.
|
| 58 |
+
- For eval predictions this means our submission_v2.csv pools all eval rows' (pre-cutoff, label-free) histories into item-level stats.
|
| 59 |
+
- That's the same pattern Grok Bot labelled a leak and superseded in its old v2.
|
| 60 |
+
- It's label-free and pre-Jan27, so it's not label leakage. But it is transductive use of the eval rows. Whether that's allowed is Eric's call.
|
| 61 |
+
|
| 62 |
+
## 2. Like-for-like scoring (our metric: global ROC AUC, v4/common.report), identical rows
|
| 63 |
+
v3 preds come from re-predicting the saved models v3/out/full25.txt and full26_lr05.txt in parquet row order.
|
| 64 |
+
The saved v3 *_valpred.npy files have an unrecoverable row order because of an unstable sort in v3/train.py. The re-prediction reproduces 0.75554 and 0.77444.
|
| 65 |
+
|
| 66 |
+
| set | model | AUC | eval-weighted | stale ≤1d | stale 1–2d | event AUC* | Spearman vs v3 |
|
| 67 |
+
|---|---|---|---|---|---|---|---|
|
| 68 |
+
| val25 (439,640) | **v3 (ours)** | 0.75554 | 0.75554 | 0.76059 | 0.73424 | 0.64894 | – |
|
| 69 |
+
| val25 | **v2work ne notime 325f** (combo_val25_notime.npy, e-fold run, 300 rounds) | **0.77749** | 0.77748 | 0.78199 | 0.75830 | 0.65403 | 0.899 |
|
| 70 |
+
| val25 | v2work notime_x2 (pre-ne config, 04:26) | 0.77536 | 0.77535 | 0.77968 | 0.75708 | 0.65328 | 0.899 |
|
| 71 |
+
| val25 | v2work screen, base, 25%-user train, 1 seed | 0.77174 | 0.77173 | 0.77642 | 0.75176 | 0.59153* | 0.888 |
|
| 72 |
+
| val26 (351,380) | **v3 (ours)** | 0.77444 | 0.77444 | 0.77444 | – | 0.65666 | – |
|
| 73 |
+
| val26 | v2work screen, base, 25%-user train | **0.78921** | 0.78921 | 0.78921 | – | 0.59846* | 0.912 |
|
| 74 |
+
|
| 75 |
+
*Event AUC = AUC of max(y) vs sum(p) per event. The screen preds are raw scores, so their sum isn't comparable; ignore those two event numbers.
|
| 76 |
+
|
| 77 |
+
Notes:
|
| 78 |
+
- The final config (f-folds, 400 rounds) scored 0.77748 in ne_combo25.log, but its preds were overwritten by the e-fold run (0.77749 at 300). The two are equivalent proxies.
|
| 79 |
+
- There are no full-data v2work val26 preds. The 25% screen alone beats our full v3 by +0.015.
|
| 80 |
+
- The gain is uniform across staleness buckets: +0.021 at ≤1d and +0.024 at 1–2d. It isn't concentrated where leakage would show.
|
| 81 |
+
|
| 82 |
+
**Final submissions:** Spearman(v2work/out/submission_v3.csv, /workspace/huawei/submission_v2.csv) = **0.918** (Pearson 0.929) over 529,390 rows (all joined). Mean p is 0.0625 vs 0.0616.
|
| 83 |
+
|
| 84 |
+
## 3. Reproduction and lag-1d leak test (parallel/lag/, one job at a time, 3 threads, seed 42, deterministic)
|
| 85 |
+
**Reproduction.**
|
| 86 |
+
- Retrained v2work's exact final config (snapshot combo.py PARAMS, notime, XF=1 XF2=1, f1..f4, 400 rounds) on v2work's on-disk tables, read only.
|
| 87 |
+
- val25 AUC was 0.77684, 0.77724 and **0.77748** at 200, 300 and 400 rounds. That's identical to ne_combo25.log, so 0.7775 is real and reproducible.
|
| 88 |
+
- I also rebuilt the val25 tables from the snapshot code (lag/repro_cmp2.json):
|
| 89 |
+
- feat_x2 is exact.
|
| 90 |
+
- feat (v2) has 31 of 122 cols with tiny diffs (corr ≥0.9997).
|
| 91 |
+
- feat_v3 has 60 of 169 co-occurrence/rank cols differing on ≤2.5% of rows (corr ≥0.989).
|
| 92 |
+
- feat_x has clctr cols differing on ≤5% of rows (corr ≥0.996).
|
| 93 |
+
- These diffs are consistent with thread-dependent randomized SVD/KMeans and tie-breaking. They don't point to a different (eval-pooled) build.
|
| 94 |
+
|
| 95 |
+
**Lag-1d test.**
|
| 96 |
+
- Same target rows (val25 = first event per user at or after Jan25, labels unchanged). All four feature builders were rerun with the cutoff frozen at **Jan24**, so no stat or label from [Jan24, Jan25) is used.
|
| 97 |
+
- Predicted with the same model (lag/lagtest.py):
|
| 98 |
+
|
| 99 |
+
| val25 features | AUC | stale ≤1d | stale 1–2d | event AUC |
|
| 100 |
+
|---|---|---|---|---|
|
| 101 |
+
| as built (C=Jan25) | 0.77748 | 0.78184 | 0.75895 | 0.65358 |
|
| 102 |
+
| lag-1d (C=Jan24) | 0.76470 | 0.76946 | 0.74430 | 0.64837 |
|
| 103 |
+
|
| 104 |
+
- The drop is −0.0128, gradual, with no cliff. It's smaller than v3's own lag-1d drop (0.7556→0.7391, −0.0165; v3/out/leakcheck.jsonl).
|
| 105 |
+
- Even with 1-day-stale features, v2work (0.7647) beats our v3 with fresh features (0.7555).
|
| 106 |
+
- A target-day leak would collapse towards ~0.65–0.70 when the target day is removed. That doesn't happen.
|
| 107 |
+
|
| 108 |
+
## 4. Verdict
|
| 109 |
+
**v2work's ne pipeline is genuinely better than our v3.**
|
| 110 |
+
- val25: +0.022 AUC (0.7775 vs 0.7555) on identical rows and labels.
|
| 111 |
+
- val26 screen: +0.015, even with a 25% train sample.
|
| 112 |
+
- The gain holds in every staleness bucket and survives the lag-1d test.
|
| 113 |
+
|
| 114 |
+
The audit found no label leakage:
|
| 115 |
+
- Training targets are before C.
|
| 116 |
+
- All label stats come from ts<C, with asserts.
|
| 117 |
+
- Item and global stats are train only.
|
| 118 |
+
- Eval rows contribute only their own given pre-cutoff history to user-level features.
|
| 119 |
+
|
| 120 |
+
Grok Bot's claims all check out:
|
| 121 |
+
- The ne variant files.
|
| 122 |
+
- 0.7775 from ne_combo25 and ne_combo25_eft.
|
| 123 |
+
- submission_v3.csv built by the 06:30 combo final run (it equals eval_pred_v3.parquet).
|
| 124 |
+
- The old v2work submission_v2 is superseded.
|
| 125 |
+
|
| 126 |
+
Correlation with our submission: Spearman 0.918.
|
| 127 |
+
|
| 128 |
+
**Flags for Eric:**
|
| 129 |
+
1. **v2work/out/submission_v3.csv appears to have been uploaded already.** v2work/out/submission_log.csv has a row "submission_v3.csv.zip (public score) … 0.7775, 0.788757", so public AUC was 0.7888. This parallel check didn't upload or submit anything.
|
| 130 |
+
2. Our v3 (and so /workspace/huawei/submission_v2.csv) builds item, co-occurrence and SVD stats from all target and eval rows' histories (v3/features.py L127/L172/L194). That's label-free but transductive, and stricter rules would call it eval-row use. v2work removed this.
|
| 131 |
+
3. Minor: v2work's extra.py L51 and L55–57 still fold target users' (pre-C, label-free) vectors into item-side aggregates and KMeans.
|
| 132 |
+
|
| 133 |
+
**Final CSV:** it exists. v2work/out/submission_v3.csv: sha256 466f3d64…, 529,390 rows, written 06:37, plus solution_v3.py and src zip.
|
| 134 |
+
- Rebuilding from scratch at ≤3 threads would take about 40–50 min: feature tables ~25 min, then final fit on 2.1M rows × 3 seeds × 400 rounds ~12–20 min (the 1.4M-row fit here took ~3 min).
|
| 135 |
+
- If all tables are reused, only the final fit is needed: ~15–20 min. I didn't build one.
|
parallel/score.log
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"val25": {
|
| 3 |
+
"rows_v3": 439640,
|
| 4 |
+
"rows_v2work": 439640,
|
| 5 |
+
"rows_common": 439640,
|
| 6 |
+
"only_v3": 0,
|
| 7 |
+
"only_v2work": 0,
|
| 8 |
+
"label_mismatch": 0,
|
| 9 |
+
"v3 (full25.txt)": {
|
| 10 |
+
"auc": 0.75554,
|
| 11 |
+
"n": 439640,
|
| 12 |
+
"auc_<=1d": 0.76059,
|
| 13 |
+
"share_<=1d": 0.80134,
|
| 14 |
+
"auc_1-2d": 0.73424,
|
| 15 |
+
"share_1-2d": 0.19866,
|
| 16 |
+
"auc_>2d": null,
|
| 17 |
+
"share_>2d": 0.0,
|
| 18 |
+
"auc_evalw": 0.75554,
|
| 19 |
+
"evalw_buckets_present": 2,
|
| 20 |
+
"auc_event": 0.64894
|
| 21 |
+
},
|
| 22 |
+
"v2work_notime_eft_ne (combo_val25_notime.npy, 300r)": {
|
| 23 |
+
"auc": 0.77749,
|
| 24 |
+
"n": 439640,
|
| 25 |
+
"auc_<=1d": 0.78199,
|
| 26 |
+
"share_<=1d": 0.80134,
|
| 27 |
+
"auc_1-2d": 0.7583,
|
| 28 |
+
"share_1-2d": 0.19866,
|
| 29 |
+
"auc_>2d": null,
|
| 30 |
+
"share_>2d": 0.0,
|
| 31 |
+
"auc_evalw": 0.77748,
|
| 32 |
+
"evalw_buckets_present": 2,
|
| 33 |
+
"auc_event": 0.65403,
|
| 34 |
+
"spearman_vs_v3": 0.899
|
| 35 |
+
},
|
| 36 |
+
"v2work_notime_x2 (04:26)": {
|
| 37 |
+
"auc": 0.77536,
|
| 38 |
+
"n": 439640,
|
| 39 |
+
"auc_<=1d": 0.77968,
|
| 40 |
+
"share_<=1d": 0.80134,
|
| 41 |
+
"auc_1-2d": 0.75708,
|
| 42 |
+
"share_1-2d": 0.19866,
|
| 43 |
+
"auc_>2d": null,
|
| 44 |
+
"share_>2d": 0.0,
|
| 45 |
+
"auc_evalw": 0.77535,
|
| 46 |
+
"evalw_buckets_present": 2,
|
| 47 |
+
"auc_event": 0.65328,
|
| 48 |
+
"spearman_vs_v3": 0.8989
|
| 49 |
+
},
|
| 50 |
+
"v2work_screen_base_25pct": {
|
| 51 |
+
"auc": 0.77174,
|
| 52 |
+
"n": 439640,
|
| 53 |
+
"auc_<=1d": 0.77642,
|
| 54 |
+
"share_<=1d": 0.80134,
|
| 55 |
+
"auc_1-2d": 0.75176,
|
| 56 |
+
"share_1-2d": 0.19866,
|
| 57 |
+
"auc_>2d": null,
|
| 58 |
+
"share_>2d": 0.0,
|
| 59 |
+
"auc_evalw": 0.77173,
|
| 60 |
+
"evalw_buckets_present": 2,
|
| 61 |
+
"auc_event": 0.59153,
|
| 62 |
+
"spearman_vs_v3": 0.8875
|
| 63 |
+
}
|
| 64 |
+
}
|
| 65 |
+
}
|
| 66 |
+
{
|
| 67 |
+
"val26": {
|
| 68 |
+
"rows_v3": 351380,
|
| 69 |
+
"rows_v2work": 351380,
|
| 70 |
+
"rows_common": 351380,
|
| 71 |
+
"only_v3": 0,
|
| 72 |
+
"only_v2work": 0,
|
| 73 |
+
"label_mismatch": 0,
|
| 74 |
+
"v3 (full26_lr05.txt)": {
|
| 75 |
+
"auc": 0.77444,
|
| 76 |
+
"n": 351380,
|
| 77 |
+
"auc_<=1d": 0.77444,
|
| 78 |
+
"share_<=1d": 1.0,
|
| 79 |
+
"auc_1-2d": null,
|
| 80 |
+
"share_1-2d": 0.0,
|
| 81 |
+
"auc_>2d": null,
|
| 82 |
+
"share_>2d": 0.0,
|
| 83 |
+
"auc_evalw": 0.77444,
|
| 84 |
+
"evalw_buckets_present": 1,
|
| 85 |
+
"auc_event": 0.65666
|
| 86 |
+
},
|
| 87 |
+
"v2work_screen_base_25pct (scr_base_c26.npy)": {
|
| 88 |
+
"auc": 0.78921,
|
| 89 |
+
"n": 351380,
|
| 90 |
+
"auc_<=1d": 0.78921,
|
| 91 |
+
"share_<=1d": 1.0,
|
| 92 |
+
"auc_1-2d": null,
|
| 93 |
+
"share_1-2d": 0.0,
|
| 94 |
+
"auc_>2d": null,
|
| 95 |
+
"share_>2d": 0.0,
|
| 96 |
+
"auc_evalw": 0.78921,
|
| 97 |
+
"evalw_buckets_present": 1,
|
| 98 |
+
"auc_event": 0.59846,
|
| 99 |
+
"spearman_vs_v3": 0.9115
|
| 100 |
+
}
|
| 101 |
+
}
|
| 102 |
+
}
|
parallel/score.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Like-for-like scoring of saved val predictions (v3 vs v2work) on identical rows. Read-only on v2work/v3."""
|
| 2 |
+
import sys, json, numpy as np, polars as pl
|
| 3 |
+
sys.path.insert(0, '/workspace/huawei/v4')
|
| 4 |
+
from common import report
|
| 5 |
+
from scipy.stats import spearmanr
|
| 6 |
+
V2='/workspace/huawei/v2work'; V3='/workspace/huawei/v3'
|
| 7 |
+
out={}
|
| 8 |
+
for cut, v3pred, v2preds in [(25, 'full25.txt', {'v2work_notime_eft_ne (combo_val25_notime.npy, 300r)':'combo_val25_notime.npy',
|
| 9 |
+
'v2work_notime_x2 (04:26)':'combo_val25_notime_x2.npy',
|
| 10 |
+
'v2work_screen_base_25pct':'scr_base_c25.npy'}),
|
| 11 |
+
(26, 'full26_lr05.txt', {'v2work_screen_base_25pct (scr_base_c26.npy)':'scr_base_c26.npy'})]:
|
| 12 |
+
# v3 val table: load_table sorts by (fold, eid) -> rows sorted by eid with stable intra-event order
|
| 13 |
+
v3 = pl.read_parquet(f'{V3}/feat/val{cut}.parquet', columns=['rowid','eid','clicked','tm_stale'])
|
| 14 |
+
v3 = v3.join(pl.read_parquet(f'/workspace/huawei/parallel/v3_val{cut}_pred.parquet'), on='rowid', how='left') # re-predicted (v3_repredict.py)
|
| 15 |
+
assert v3['p_v3'].null_count()==0
|
| 16 |
+
v2 = pl.read_parquet(f'{V2}/feat_v3/val{cut}.parquet', columns=['rowid','eid','clicked','tm_stale'])
|
| 17 |
+
for nm,f in v2preds.items():
|
| 18 |
+
p=np.load(f'{V2}/{f}'); assert len(p)==v2.height, (f,len(p),v2.height); v2=v2.with_columns(pl.Series(nm,p))
|
| 19 |
+
rs3=set(v3['rowid'].to_list()); rs2=set(v2['rowid'].to_list())
|
| 20 |
+
j = v3.join(v2.drop('eid','tm_stale').rename({'clicked':'clicked2'}), on='rowid', how='inner')
|
| 21 |
+
res={'rows_v3':v3.height,'rows_v2work':v2.height,'rows_common':j.height,'only_v3':len(rs3-rs2),'only_v2work':len(rs2-rs3),
|
| 22 |
+
'label_mismatch':int((j['clicked']!=j['clicked2']).sum())}
|
| 23 |
+
res['v3 ('+v3pred+')']=report(j, j['p_v3'].to_numpy())
|
| 24 |
+
for nm in v2preds:
|
| 25 |
+
res[nm]=report(j, j[nm].to_numpy())
|
| 26 |
+
res[nm]['spearman_vs_v3']=round(float(spearmanr(j[nm].to_numpy(), j['p_v3'].to_numpy()).statistic),4)
|
| 27 |
+
out[f'val{cut}']=res
|
| 28 |
+
print(json.dumps({f'val{cut}':res},indent=1),flush=True)
|
| 29 |
+
json.dump(out,open('/workspace/huawei/parallel/score_val.json','w'),indent=1)
|
parallel/score_val.json
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"val25": {
|
| 3 |
+
"rows_v3": 439640,
|
| 4 |
+
"rows_v2work": 439640,
|
| 5 |
+
"rows_common": 439640,
|
| 6 |
+
"only_v3": 0,
|
| 7 |
+
"only_v2work": 0,
|
| 8 |
+
"label_mismatch": 0,
|
| 9 |
+
"v3 (full25.txt)": {
|
| 10 |
+
"auc": 0.75554,
|
| 11 |
+
"n": 439640,
|
| 12 |
+
"auc_<=1d": 0.76059,
|
| 13 |
+
"share_<=1d": 0.80134,
|
| 14 |
+
"auc_1-2d": 0.73424,
|
| 15 |
+
"share_1-2d": 0.19866,
|
| 16 |
+
"auc_>2d": null,
|
| 17 |
+
"share_>2d": 0.0,
|
| 18 |
+
"auc_evalw": 0.75554,
|
| 19 |
+
"evalw_buckets_present": 2,
|
| 20 |
+
"auc_event": 0.64894
|
| 21 |
+
},
|
| 22 |
+
"v2work_notime_eft_ne (combo_val25_notime.npy, 300r)": {
|
| 23 |
+
"auc": 0.77749,
|
| 24 |
+
"n": 439640,
|
| 25 |
+
"auc_<=1d": 0.78199,
|
| 26 |
+
"share_<=1d": 0.80134,
|
| 27 |
+
"auc_1-2d": 0.7583,
|
| 28 |
+
"share_1-2d": 0.19866,
|
| 29 |
+
"auc_>2d": null,
|
| 30 |
+
"share_>2d": 0.0,
|
| 31 |
+
"auc_evalw": 0.77748,
|
| 32 |
+
"evalw_buckets_present": 2,
|
| 33 |
+
"auc_event": 0.65403,
|
| 34 |
+
"spearman_vs_v3": 0.899
|
| 35 |
+
},
|
| 36 |
+
"v2work_notime_x2 (04:26)": {
|
| 37 |
+
"auc": 0.77536,
|
| 38 |
+
"n": 439640,
|
| 39 |
+
"auc_<=1d": 0.77968,
|
| 40 |
+
"share_<=1d": 0.80134,
|
| 41 |
+
"auc_1-2d": 0.75708,
|
| 42 |
+
"share_1-2d": 0.19866,
|
| 43 |
+
"auc_>2d": null,
|
| 44 |
+
"share_>2d": 0.0,
|
| 45 |
+
"auc_evalw": 0.77535,
|
| 46 |
+
"evalw_buckets_present": 2,
|
| 47 |
+
"auc_event": 0.65328,
|
| 48 |
+
"spearman_vs_v3": 0.8989
|
| 49 |
+
},
|
| 50 |
+
"v2work_screen_base_25pct": {
|
| 51 |
+
"auc": 0.77174,
|
| 52 |
+
"n": 439640,
|
| 53 |
+
"auc_<=1d": 0.77642,
|
| 54 |
+
"share_<=1d": 0.80134,
|
| 55 |
+
"auc_1-2d": 0.75176,
|
| 56 |
+
"share_1-2d": 0.19866,
|
| 57 |
+
"auc_>2d": null,
|
| 58 |
+
"share_>2d": 0.0,
|
| 59 |
+
"auc_evalw": 0.77173,
|
| 60 |
+
"evalw_buckets_present": 2,
|
| 61 |
+
"auc_event": 0.59153,
|
| 62 |
+
"spearman_vs_v3": 0.8875
|
| 63 |
+
}
|
| 64 |
+
},
|
| 65 |
+
"val26": {
|
| 66 |
+
"rows_v3": 351380,
|
| 67 |
+
"rows_v2work": 351380,
|
| 68 |
+
"rows_common": 351380,
|
| 69 |
+
"only_v3": 0,
|
| 70 |
+
"only_v2work": 0,
|
| 71 |
+
"label_mismatch": 0,
|
| 72 |
+
"v3 (full26_lr05.txt)": {
|
| 73 |
+
"auc": 0.77444,
|
| 74 |
+
"n": 351380,
|
| 75 |
+
"auc_<=1d": 0.77444,
|
| 76 |
+
"share_<=1d": 1.0,
|
| 77 |
+
"auc_1-2d": null,
|
| 78 |
+
"share_1-2d": 0.0,
|
| 79 |
+
"auc_>2d": null,
|
| 80 |
+
"share_>2d": 0.0,
|
| 81 |
+
"auc_evalw": 0.77444,
|
| 82 |
+
"evalw_buckets_present": 1,
|
| 83 |
+
"auc_event": 0.65666
|
| 84 |
+
},
|
| 85 |
+
"v2work_screen_base_25pct (scr_base_c26.npy)": {
|
| 86 |
+
"auc": 0.78921,
|
| 87 |
+
"n": 351380,
|
| 88 |
+
"auc_<=1d": 0.78921,
|
| 89 |
+
"share_<=1d": 1.0,
|
| 90 |
+
"auc_1-2d": null,
|
| 91 |
+
"share_1-2d": 0.0,
|
| 92 |
+
"auc_>2d": null,
|
| 93 |
+
"share_>2d": 0.0,
|
| 94 |
+
"auc_evalw": 0.78921,
|
| 95 |
+
"evalw_buckets_present": 1,
|
| 96 |
+
"auc_event": 0.59846,
|
| 97 |
+
"spearman_vs_v3": 0.9115
|
| 98 |
+
}
|
| 99 |
+
}
|
| 100 |
+
}
|
parallel/src_snapshot/v2work/blend_val.py
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Compare v2 (my features) vs v3 features vs blend on the shared validation:
|
| 2 |
+
cutoff Jan25, targets = first event per user in [Jan25, Jan27); training rows = first-event folds with cutoffs Jan21..Jan24 (W=1d)."""
|
| 3 |
+
import sys, os, numpy as np, polars as pl, lightgbm as lgb, json
|
| 4 |
+
sys.path.insert(0,'/workspace/huawei/v2work'); sys.path.insert(0,'/workspace/huawei/v3')
|
| 5 |
+
from feats import DROP
|
| 6 |
+
from features import feature_cols, GROUPS
|
| 7 |
+
from sklearn.metrics import roc_auc_score
|
| 8 |
+
from scipy.stats import rankdata
|
| 9 |
+
P2='/workspace/huawei/v2work/feat'; P3='/workspace/huawei/v3/feat'
|
| 10 |
+
def fit_pred(tr, va, feats, params, rounds, ev_every=25):
|
| 11 |
+
Xtr=tr.select(feats).to_numpy().astype(np.float32); Xva=va.select(feats).to_numpy().astype(np.float32); y=va['clicked'].to_numpy()
|
| 12 |
+
m=lgb.train(params,lgb.Dataset(Xtr,tr['clicked'].to_numpy(),feature_name=feats),num_boost_round=rounds)
|
| 13 |
+
curve=[(k,roc_auc_score(y,m.predict(Xva,num_iteration=k))) for k in range(ev_every,rounds+1,ev_every)]
|
| 14 |
+
return m,curve,Xva
|
| 15 |
+
base=dict(objective='binary',num_leaves=63,min_data_in_leaf=200,feature_fraction=0.7,bagging_fraction=0.8,bagging_freq=1,lambda_l2=10.0,verbose=-1,seed=42,num_threads=8)
|
| 16 |
+
# ---- v3 features
|
| 17 |
+
tr3=pl.concat([pl.read_parquet(f'{P3}/f{k}.parquet') for k in (1,2,3,4)],how='diagonal_relaxed')
|
| 18 |
+
va3=pl.read_parquet(f'{P3}/val25.parquet')
|
| 19 |
+
f3=feature_cols(tr3,GROUPS)
|
| 20 |
+
m3,c3,X3=fit_pred(tr3,va3,f3,dict(base,learning_rate=0.05),500)
|
| 21 |
+
print('v3 curve',[(k,round(a,5)) for k,a in c3],flush=True)
|
| 22 |
+
b3=max(c3,key=lambda x:x[1]); p3=m3.predict(X3,num_iteration=b3[0])
|
| 23 |
+
# ---- v2 features
|
| 24 |
+
tr2=pl.concat([pl.read_parquet(f'{P2}/h{k}.parquet') for k in (1,2,3,4)],how='diagonal_relaxed')
|
| 25 |
+
va2=pl.read_parquet(f'{P2}/val25.parquet')
|
| 26 |
+
f2=[c for c in tr2.columns if c not in DROP]
|
| 27 |
+
m2,c2,X2=fit_pred(tr2,va2,f2,dict(base,learning_rate=0.05),500)
|
| 28 |
+
print('v2 curve',[(k,round(a,5)) for k,a in c2],flush=True)
|
| 29 |
+
b2=max(c2,key=lambda x:x[1]); p2=m2.predict(X2,num_iteration=b2[0])
|
| 30 |
+
# ---- combined feature set (v3 + v2 features not in v3), joined on rowid
|
| 31 |
+
extra=va2.select(['rowid']+[c for c in f2]).rename({c:'v2_'+c for c in f2})
|
| 32 |
+
trc=tr3.join(tr2.select(['rowid']+f2).rename({c:'v2_'+c for c in f2}),on='rowid',how='left')
|
| 33 |
+
vac=va3.join(extra,on='rowid',how='left')
|
| 34 |
+
fc=f3+['v2_'+c for c in f2]
|
| 35 |
+
mc,cc,Xc=fit_pred(trc,vac,fc,dict(base,learning_rate=0.05),500)
|
| 36 |
+
print('combined curve',[(k,round(a,5)) for k,a in cc],flush=True)
|
| 37 |
+
# ---- blends on rowid
|
| 38 |
+
a=pl.DataFrame({'rowid':va3['rowid'],'p3':p3,'y':va3['clicked']}).join(pl.DataFrame({'rowid':va2['rowid'],'p2':p2}),on='rowid')
|
| 39 |
+
y=a['y'].to_numpy(); r3=rankdata(a['p3'])/len(a); r2=rankdata(a['p2'])/len(a)
|
| 40 |
+
res=dict(v3=b3,v2=b2,combined=max(cc,key=lambda x:x[1]))
|
| 41 |
+
for w in (0.5,0.6,0.7,0.8,0.9): res[f'rankblend_w3={w}']=roc_auc_score(y,w*r3+(1-w)*r2)
|
| 42 |
+
print(json.dumps(res,indent=1))
|
| 43 |
+
imp=sorted(zip(fc,mc.feature_importance('gain')),key=lambda x:-x[1]); tot=sum(v for _,v in imp)
|
| 44 |
+
print('combined top', [(f,round(v/tot,4)) for f,v in imp[:25]])
|
parallel/src_snapshot/v2work/build_folds.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import polars as pl, sys, os
|
| 2 |
+
sys.path.insert(0,'/workspace/huawei/v2work')
|
| 3 |
+
from feats import load, first_events, build, log
|
| 4 |
+
D='/workspace/huawei/v2work'; os.makedirs(f'{D}/feat',exist_ok=True)
|
| 5 |
+
TE,TR,EE,ER,NI=load(D)
|
| 6 |
+
DAY=86400; J20=1737331200
|
| 7 |
+
END=J20+7*DAY; VC=J20+4*DAY
|
| 8 |
+
# validation: V = first event/user in [Jan24,Jan27); training folds end before Jan24
|
| 9 |
+
specs=[(VC,END-VC,'V')]+[(J20+k*DAY,VC-(J20+k*DAY),f'g{k}') for k in (1,2,3)]
|
| 10 |
+
if len(sys.argv)>1 and sys.argv[1]=='c25':
|
| 11 |
+
specs=[(J20+5*DAY,2*DAY,'val25')]+[(J20+k*DAY,DAY,f'h{k}') for k in (1,2,3,4)]
|
| 12 |
+
if len(sys.argv)>1 and sys.argv[1]=='c26':
|
| 13 |
+
specs=[(J20+6*DAY,DAY,'val26')]+[(J20+k*DAY,DAY,f'h{k}') for k in (5,)]+[('eval',None,'eval')]
|
| 14 |
+
if len(sys.argv)>1 and sys.argv[1]=='eft':
|
| 15 |
+
specs=[(J20+k*DAY,(J20+5*DAY)-(J20+k*DAY),f'e{k}') for k in (1,2,3,4)]+[(J20+k*DAY,END-(J20+k*DAY),f'E{k}') for k in (1,2,3,4,5,6)]
|
| 16 |
+
if len(sys.argv)>1 and sys.argv[1]=='final':
|
| 17 |
+
specs=[(J20+k*DAY,END-(J20+k*DAY),f'F{k}') for k in (1,2,3,4,5,6)]+[('eval',None,'eval')]
|
| 18 |
+
for C,W,name in specs:
|
| 19 |
+
out=f'{D}/feat/{name}.parquet'
|
| 20 |
+
if os.path.exists(out): continue
|
| 21 |
+
if C=='eval':
|
| 22 |
+
C=J20+7*DAY; T_ev=EE; T_rows=ER
|
| 23 |
+
else:
|
| 24 |
+
T_ev=first_events(TE,C,W); T_rows=TR.join(T_ev.select('eid'),on='eid')
|
| 25 |
+
R=build(C,T_ev,T_rows,TE,TR,NI)
|
| 26 |
+
R.drop('h','h_ts').write_parquet(out)
|
| 27 |
+
log('wrote',name,R.shape)
|
parallel/src_snapshot/v2work/combo.py
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Combined (v3 features + v2 features) LightGBM. Modes:
|
| 2 |
+
python combo.py val 25|26 [variant] -> fixed-round curve on val25/val26, segment + weekday/weekend AUCs
|
| 3 |
+
python combo.py final N_ITER SEEDS -> fit on all 6 cutoff folds (Jan21..Jan26), predict eval (cutoff Jan27)
|
| 4 |
+
variants: base | notime (drop all time/gap feats) | wkw (weekend train events weight 0.3) | notime_wkw"""
|
| 5 |
+
import sys, os, json, numpy as np, polars as pl, lightgbm as lgb
|
| 6 |
+
sys.path.insert(0,'/workspace/huawei/v2work'); sys.path.insert(0,'/workspace/huawei/v3')
|
| 7 |
+
from feats import DROP
|
| 8 |
+
from features import feature_cols, GROUPS
|
| 9 |
+
from sklearn.metrics import roc_auc_score
|
| 10 |
+
P2='/workspace/huawei/v2work/feat'; P3='/workspace/huawei/v2work/feat_v3'
|
| 11 |
+
V2_OF={'f1':'h1','f2':'h2','f3':'h3','f4':'h4','f5':'h5','f6':'val26','val25':'val25','val26':'val26','eval':'eval'}
|
| 12 |
+
V2_OF.update({f'{p}{k}':f'{p}{k}' for p in 'eE' for k in range(1,7)})
|
| 13 |
+
EFT=os.environ.get('FOLDS')=='eft' # eval-like multi-day target windows (e1..e4 -> Jan25, E1..E6 -> Jan27)
|
| 14 |
+
OUTP=os.environ.get('OUTP','eval_pred')
|
| 15 |
+
TIME=['tm_stale','tm_hour','tm_tb','tm_dt','v2_tb','v2_dt','v2_stale','v2_hour']
|
| 16 |
+
def load(names):
|
| 17 |
+
out=[]
|
| 18 |
+
for n in names:
|
| 19 |
+
a=pl.read_parquet(f'{P3}/{n}.parquet'); b=pl.read_parquet(f'{P2}/{V2_OF[n]}.parquet')
|
| 20 |
+
f2=[c for c in b.columns if c not in DROP]
|
| 21 |
+
b=b.select(['rowid']+f2).rename({c:'v2_'+c for c in f2})
|
| 22 |
+
a=a.join(b,on='rowid',how='left')
|
| 23 |
+
if os.environ.get('XF'):
|
| 24 |
+
a=a.join(pl.read_parquet(f'/workspace/huawei/v2work/feat_x/{n}.parquet'),on='rowid',how='left')
|
| 25 |
+
if os.environ.get('XF2'):
|
| 26 |
+
a=a.join(pl.read_parquet(f'/workspace/huawei/v2work/feat_x2/{n}.parquet'),on='rowid',how='left')
|
| 27 |
+
out.append(a)
|
| 28 |
+
return pl.concat(out,how='diagonal_relaxed')
|
| 29 |
+
def featlist(df,variant):
|
| 30 |
+
f=feature_cols(df,GROUPS)+[c for c in df.columns if c.startswith(('v2_','x_','r_','s_'))]
|
| 31 |
+
if 'notime' in variant: f=[c for c in f if c not in TIME]
|
| 32 |
+
return f
|
| 33 |
+
def weights(df,variant):
|
| 34 |
+
if 'wkw' not in variant: return None
|
| 35 |
+
dow=((df['ts'].to_numpy()//86400)+3)%7 # 0=Mon (1970-01-01 was Thu)
|
| 36 |
+
return np.where(dow>=5,0.3,1.0)
|
| 37 |
+
PARAMS=dict(objective='binary',learning_rate=0.05,num_leaves=63,min_data_in_leaf=200,feature_fraction=0.7,bagging_fraction=0.8,
|
| 38 |
+
bagging_freq=1,lambda_l2=10.0,verbose=-1,seed=42,num_threads=8)
|
| 39 |
+
mode=sys.argv[1]
|
| 40 |
+
if mode=='val':
|
| 41 |
+
cut=int(sys.argv[2]); variant=sys.argv[3] if len(sys.argv)>3 else 'base'
|
| 42 |
+
import gc
|
| 43 |
+
feats=None; Xs=[]; ys=[]
|
| 44 |
+
for k in range(1,cut-20):
|
| 45 |
+
t=load([f'e{k}' if EFT else f'f{k}'])
|
| 46 |
+
if feats is None: feats=featlist(t,variant)
|
| 47 |
+
Xs.append(t.select(feats).to_numpy().astype(np.float32)); ys.append(t['clicked'].to_numpy()); del t; gc.collect()
|
| 48 |
+
X=np.concatenate(Xs); del Xs; gc.collect()
|
| 49 |
+
ds=lgb.Dataset(X,np.concatenate(ys),feature_name=feats,free_raw_data=True).construct(); del X; gc.collect()
|
| 50 |
+
m=lgb.train(PARAMS,ds,num_boost_round=450); del ds; gc.collect()
|
| 51 |
+
va=load([f'val{cut}'])
|
| 52 |
+
Xv=va.select(feats).to_numpy().astype(np.float32); y=va['clicked'].to_numpy()
|
| 53 |
+
curve={k:roc_auc_score(y,m.predict(Xv,num_iteration=k)) for k in range(50,451,50)}
|
| 54 |
+
best=max(curve,key=curve.get); p=m.predict(Xv,num_iteration=300)
|
| 55 |
+
dow=((va['ts'].to_numpy()//86400)+3)%7
|
| 56 |
+
cold=va['us_cold'].to_numpy()==1; noh=va['us_nohist'].to_numpy()==1
|
| 57 |
+
segs={'all':np.ones(len(y),bool),'warm':~cold,'history_only':cold&~noh,'empty_history':noh,'weekday':dow<5,'weekend':dow>=5,'stale_lt1d':va['tm_stale'].to_numpy()<86400,'stale_ge1d':va['tm_stale'].to_numpy()>=86400}
|
| 58 |
+
seg={k:(int(s.sum()),round(roc_auc_score(y[s],p[s]),5) if s.sum()>0 and y[s].min()<y[s].max() else None) for k,s in segs.items()}
|
| 59 |
+
g=m.feature_importance('gain'); top=[(feats[i],round(float(g[i]/g.sum()),4)) for i in np.argsort(-g)[:20]]
|
| 60 |
+
res=dict(cut=cut,variant=variant+('_eft' if EFT else '')+('_ne'),n_feats=len(feats),curve={k:round(v,5) for k,v in curve.items()},best=best,seg_at300=seg,top=top)
|
| 61 |
+
print(json.dumps(res),flush=True)
|
| 62 |
+
open('/workspace/huawei/v2work/combo_results.jsonl','a').write(json.dumps(res)+'\n')
|
| 63 |
+
np.save(f'/workspace/huawei/v2work/combo_val{cut}_{variant}.npy',p)
|
| 64 |
+
elif mode=='final':
|
| 65 |
+
import gc
|
| 66 |
+
n_iter=int(sys.argv[2]); seeds=int(sys.argv[3]); variant=sys.argv[4] if len(sys.argv)>4 else 'base'
|
| 67 |
+
feats=None; Xs=[]; ys=[]; ws=[]
|
| 68 |
+
for k in range(1,7): # load fold by fold -> float32 numpy to keep memory low
|
| 69 |
+
t=load([f'E{k}' if EFT else f'f{k}'])
|
| 70 |
+
if feats is None: feats=featlist(t,variant)
|
| 71 |
+
Xs.append(t.select(feats).to_numpy().astype(np.float32)); ys.append(t['clicked'].to_numpy())
|
| 72 |
+
w=weights(t,variant); ws.append(w if w is not None else np.ones(len(t)))
|
| 73 |
+
del t; gc.collect()
|
| 74 |
+
X=np.concatenate(Xs); del Xs; y=np.concatenate(ys); w=np.concatenate(ws) if 'wkw' in variant else None
|
| 75 |
+
print('train rows',len(y),'feats',len(feats),flush=True)
|
| 76 |
+
ds=lgb.Dataset(X,y,weight=w,feature_name=feats,free_raw_data=True).construct(); del X; gc.collect()
|
| 77 |
+
preds=[]; ms=[]
|
| 78 |
+
for s in range(seeds):
|
| 79 |
+
m=lgb.train(dict(PARAMS,seed=42+s),ds,num_boost_round=n_iter)
|
| 80 |
+
m.save_model(f'/workspace/huawei/v2work/{OUTP}_model_s{s}.txt'); ms.append(m); print('seed',s,'done',flush=True)
|
| 81 |
+
del ds; gc.collect()
|
| 82 |
+
ev=load(['eval']); Xe=ev.select(feats).to_numpy().astype(np.float32)
|
| 83 |
+
p=np.mean([m.predict(Xe) for m in ms],0) # plain probability average across seeds; no per-event normalisation
|
| 84 |
+
pl.DataFrame({'rowid':ev['rowid'],'p':p}).write_parquet(f'/workspace/huawei/v2work/{OUTP}.parquet')
|
| 85 |
+
g=m.feature_importance('gain'); print('top',[(feats[i],round(float(g[i]/g.sum()),4)) for i in np.argsort(-g)[:20]])
|
parallel/src_snapshot/v2work/emb_exp.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import polars as pl, numpy as np, sys
|
| 2 |
+
sys.path.insert(0,'/workspace/huawei/v2work')
|
| 3 |
+
from feats import load, first_events
|
| 4 |
+
from scipy import sparse
|
| 5 |
+
from sklearn.utils.extmath import randomized_svd
|
| 6 |
+
from sklearn.metrics import roc_auc_score
|
| 7 |
+
TE,TR,EE,ER,NI=load('/workspace/huawei/v2work')
|
| 8 |
+
DAY=86400; C=1737331200+4*DAY
|
| 9 |
+
T_ev=first_events(TE,C,DAY); T_rows=TR.join(T_ev.select('eid'),on='eid')
|
| 10 |
+
P_ev=TE.filter(pl.col('ts')<C); P=TR.join(P_ev.select('eid','user','ts'),on='eid')
|
| 11 |
+
hc=lambda E: E.select('user','h','h_ts').explode(['h','h_ts']).drop_nulls().rename({'h':'ii','h_ts':'ts'})
|
| 12 |
+
pool=pl.concat([hc(P_ev),hc(T_ev),P.filter(pl.col('clicked')==1).select('user','ii','ts')]).unique()
|
| 13 |
+
us=pool.group_by('user','ii').agg(pl.len().alias('n'))
|
| 14 |
+
uidx=us.select('user').unique().with_row_index('ur'); m=us.join(uidx,on='user')
|
| 15 |
+
R=T_rows.join(T_ev.select('eid','user','h'),on='eid').sort('eid')
|
| 16 |
+
y=R['clicked'].to_numpy(); eid=R['eid'].to_numpy()
|
| 17 |
+
ex=R.with_row_index('r').select('r','h').with_columns(pl.int_ranges(pl.col('h').list.len(),0,-1).alias('pos')).explode(['h','pos']).drop_nulls()
|
| 18 |
+
rr=ex['r'].to_numpy(); hh=ex['h'].to_numpy(); pos=ex['pos'].to_numpy()
|
| 19 |
+
um=R.select('user').join(uidx,on='user',how='left')['ur'].fill_null(0).to_numpy()
|
| 20 |
+
def dm(x):
|
| 21 |
+
s=pl.DataFrame({'e':eid,'x':x}).with_columns((pl.col('x')-pl.col('x').mean().over('e')).alias('d'))['d'].to_numpy(); return s
|
| 22 |
+
def ev(name,V,U=None,decay=0.85):
|
| 23 |
+
hv=np.zeros((len(R),V.shape[1]),np.float32); np.add.at(hv,rr,V[hh]*(decay**(pos-1))[:,None])
|
| 24 |
+
hvn=hv/(np.linalg.norm(hv,axis=1,keepdims=True)+1e-6); c=V[R['ii'].to_numpy()]
|
| 25 |
+
s=(hvn*c).sum(1); out=f'{name}: hist cos {roc_auc_score(y,s):.4f} dm {roc_auc_score(y,dm(s)):.4f}'
|
| 26 |
+
if U is not None:
|
| 27 |
+
s2=(U[um]*c).sum(1); out+=f' | ucos {roc_auc_score(y,s2):.4f} dm {roc_auc_score(y,dm(s2)):.4f}'
|
| 28 |
+
print(out,flush=True)
|
| 29 |
+
ii=m['ii'].to_numpy(); ur=m['ur'].to_numpy()
|
| 30 |
+
df_=np.bincount(ii,minlength=NI); idf=np.log((len(uidx)+1)/(df_+1)).astype(np.float32)
|
| 31 |
+
for wname,w in [('bin',np.ones(len(ii),np.float32)),('idf',idf[ii])]:
|
| 32 |
+
X=sparse.csr_matrix((w,(ur,ii)),shape=(len(uidx),NI)); rn=np.sqrt(np.asarray(X.multiply(X).sum(1)).ravel())+1e-6; X=sparse.diags(1/rn)@X
|
| 33 |
+
for k in (128,256):
|
| 34 |
+
U,S,Vt=randomized_svd(X,k,random_state=0,n_iter=4)
|
| 35 |
+
for a in (0.0,):
|
| 36 |
+
V=(Vt.T*S**a).astype(np.float32); Vn=V/(np.linalg.norm(V,axis=1,keepdims=True)+1e-6)
|
| 37 |
+
Uf=(U*S**a).astype(np.float32); Uf/=np.linalg.norm(Uf,axis=1,keepdims=True)+1e-6
|
| 38 |
+
ev(f'{wname} k={k} a={a}',Vn,Uf)
|
parallel/src_snapshot/v2work/explore.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import polars as pl
|
| 2 |
+
tr=pl.read_parquet('train_rows.parquet'); te=pl.read_parquet('train_events.parquet')
|
| 3 |
+
er=pl.read_parquet('evaluation_rows.parquet'); ee=pl.read_parquet('evaluation_events.parquet')
|
| 4 |
+
print(tr.shape, te.shape, er.shape, ee.shape)
|
| 5 |
+
print('uniq eid', tr['eid'].n_unique(), te['eid'].n_unique(), ee['eid'].n_unique())
|
| 6 |
+
print('ctr', tr['clicked'].mean())
|
| 7 |
+
ev=tr.group_by('eid').agg(pl.col('clicked').sum().alias('c'))
|
| 8 |
+
print('clicks/event dist', ev['c'].value_counts().sort('c'))
|
| 9 |
+
print('users train', te['user'].n_unique(), 'eval', ee['user'].n_unique(), 'eval users in train', ee['user'].is_in(te['user'].implode()).sum())
|
| 10 |
+
print('items train', tr['item'].n_unique(), 'eval', er['item'].n_unique(), 'eval items in train', er['item'].unique().is_in(tr['item'].implode()).sum())
|
| 11 |
+
print('eval rows with item seen', er['item'].is_in(tr['item'].implode()).mean())
|
| 12 |
+
print('hist len train', te['h_items'].list.len().describe())
|
| 13 |
+
print('hist len eval', ee['h_items'].list.len().describe())
|
| 14 |
+
print('events/user', te.group_by('user').len()['len'].describe())
|
| 15 |
+
print('ts range', te['ts'].min(), te['ts'].max(), ee['ts'].min(), ee['ts'].max())
|
| 16 |
+
print(ee.with_columns(pl.from_epoch('ts')).select(pl.col('ts').dt.date()).to_series().value_counts().sort('ts'))
|
| 17 |
+
print(te.with_columns(pl.from_epoch('ts')).select(pl.col('ts').dt.date()).to_series().value_counts().sort('ts'))
|
| 18 |
+
# candidate in history rate
|
| 19 |
+
x=tr.join(te.select('eid','h_items'),on='eid')
|
| 20 |
+
x=x.with_columns(pl.col('h_items').list.contains(pl.col('item')).alias('inh'))
|
| 21 |
+
print(x.group_by('inh').agg(pl.len(), pl.col('clicked').mean()))
|
| 22 |
+
# items per event duplicate?
|
| 23 |
+
print('dup items in event', tr.group_by('eid').agg(pl.col('item').n_unique().alias('n'))['n'].value_counts())
|
| 24 |
+
# item impressions distribution
|
| 25 |
+
ic=tr.group_by('item').agg(pl.len().alias('n'), pl.col('clicked').mean().alias('ctr'))
|
| 26 |
+
print(ic.describe())
|
parallel/src_snapshot/v2work/extra.py
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Extra leak-free features on v3 data (all stats from data with ts < C).
|
| 2 |
+
Usage: python extra.py NAME... (names: f1..f6, val25, val26, eval) -> v2work/feat_x/NAME.parquet
|
| 3 |
+
- con variants: k=32/128, recency-weighted (tau=2d) click/non-click centroids, item-side con (users who clicked i vs skipped i)
|
| 4 |
+
- user-cluster item CTR (kmeans on user SVD vectors, 64 & 256 clusters), smoothed toward item CTR
|
| 5 |
+
- within-event z-score and rank for each"""
|
| 6 |
+
import sys, os, time, numpy as np, polars as pl
|
| 7 |
+
from scipy import sparse
|
| 8 |
+
from sklearn.utils.extmath import randomized_svd
|
| 9 |
+
from sklearn.cluster import MiniBatchKMeans
|
| 10 |
+
sys.path.insert(0, '/workspace/huawei/v3')
|
| 11 |
+
from features import load, click_log
|
| 12 |
+
from split import J20, DAY, first_events
|
| 13 |
+
OUT = '/workspace/huawei/v2work/feat_x'; os.makedirs(OUT, exist_ok=True)
|
| 14 |
+
TE, TR, EE, ER, NI, NU = load('/workspace/huawei/v3/data')
|
| 15 |
+
SPEC = {**{f'f{k}': (J20 + k * DAY, DAY) for k in range(1, 7)}, 'val25': (J20 + 5 * DAY, None), 'val26': (J20 + 6 * DAY, None), 'eval': (J20 + 7 * DAY, 'eval'),
|
| 16 |
+
**{f'e{k}': (J20 + k * DAY, (5 - k) * DAY) for k in range(1, 5)}, **{f'E{k}': (J20 + k * DAY, (7 - k) * DAY) for k in range(1, 7)}}
|
| 17 |
+
def nrm(M): return M / (np.linalg.norm(M, axis=1, keepdims=True) + 1e-9)
|
| 18 |
+
def csr(u, i, w): return sparse.csr_matrix((w.astype(np.float32), (u, i)), shape=(NU, NI))
|
| 19 |
+
def build(C, T_ev, T_rows):
|
| 20 |
+
P_ev = TE.filter(pl.col('ts') < C)
|
| 21 |
+
P_rows = TR.join(P_ev.select('eid', 'u', 'ts'), on='eid', how='inner')
|
| 22 |
+
CL = click_log(C, P_ev, P_rows, T_ev) # user-level: train data + each target row's own history
|
| 23 |
+
CLg = click_log(C, P_ev, P_rows, T_ev.head(0)) # global/item-level: train data only
|
| 24 |
+
assert CL['ts'].max() < C and P_rows['ts'].max() < C
|
| 25 |
+
R = T_rows.join(T_ev.select('eid', 'u'), on='eid', how='inner').select('rowid', 'eid', 'u', 'ii')
|
| 26 |
+
uu = R['u'].to_numpy().astype(np.int64); ii = R['ii'].to_numpy().astype(np.int64)
|
| 27 |
+
cg = CLg.group_by('u', 'ii').agg(pl.len().alias('n'))
|
| 28 |
+
Xc = csr(cg['u'].to_numpy(), cg['ii'].to_numpy(), np.ones(cg.height)); Scg = Xc.copy()
|
| 29 |
+
cnt = CL.group_by('u', 'ii').agg(pl.len().alias('n'), pl.col('ts').max().alias('lt'))
|
| 30 |
+
cu = cnt['u'].to_numpy(); ci = cnt['ii'].to_numpy()
|
| 31 |
+
df_ = np.asarray(Xc.sum(0)).ravel(); idf = np.log((NU + 1) / (df_ + 1)).astype(np.float32)
|
| 32 |
+
Xc = Xc @ sparse.diags(idf); Xc = sparse.diags(1 / (np.sqrt(np.asarray(Xc.multiply(Xc).sum(1)).ravel()) + 1e-6)) @ Xc
|
| 33 |
+
Xu = csr(cu, ci, np.ones(cnt.height)) @ sparse.diags(idf); Xu = sparse.diags(1 / (np.sqrt(np.asarray(Xu.multiply(Xu).sum(1)).ravel()) + 1e-6)) @ Xu
|
| 34 |
+
neg = P_rows.filter(pl.col('clicked') == 0)
|
| 35 |
+
nu_ = neg['u'].to_numpy(); ni_ = neg['ii'].to_numpy()
|
| 36 |
+
Sc = csr(cu, ci, np.ones(cnt.height)); Sn = csr(nu_, ni_, np.ones(neg.height))
|
| 37 |
+
rc = np.exp(-(C - cnt['lt'].to_numpy()) / (2 * DAY)); rn = np.exp(-(C - neg['ts'].to_numpy()) / (2 * DAY))
|
| 38 |
+
Scr = csr(cu, ci, rc); Snr = csr(nu_, ni_, rn)
|
| 39 |
+
F = {}
|
| 40 |
+
for k in (32, 64, 128):
|
| 41 |
+
U, S, Vt = randomized_svd(Xc, k, random_state=0, n_iter=5)
|
| 42 |
+
Vn = nrm(Vt.T * np.sqrt(S)); Un = nrm(np.asarray(Xu @ (Vt.T / np.sqrt(S)))); cand = Vn[ii] # user fold-in
|
| 43 |
+
nC = nrm(np.asarray(Sc @ Vn)); nN = nrm(np.asarray(Sn @ Vn))
|
| 44 |
+
F[f'x_con05_k{k}'] = (nrm(nC - 0.5 * nN)[uu] * cand).sum(1)
|
| 45 |
+
if k == 64:
|
| 46 |
+
nCr = nrm(np.asarray(Scr @ Vn)); nNr = nrm(np.asarray(Snr @ Vn))
|
| 47 |
+
for a, nm in ((0.5, '05'), (1.0, '1')):
|
| 48 |
+
F[f'x_conr{nm}_k64'] = (nrm(nCr - a * nNr)[uu] * cand).sum(1)
|
| 49 |
+
F['x_crcos_k64'] = (nCr[uu] * cand).sum(1)
|
| 50 |
+
# item side: centroid of users who clicked item minus users who skipped it, vs target user vector
|
| 51 |
+
iC = nrm(np.asarray(Scg.T @ Un)); iN = nrm(np.asarray(Sn.T @ Un)) # item side: train-only clicks
|
| 52 |
+
F['x_icon05_k64'] = (nrm(iC - 0.5 * iN)[ii] * Un[uu]).sum(1)
|
| 53 |
+
F['x_icos_k64'] = (iC[ii] * Un[uu]).sum(1)
|
| 54 |
+
# user-cluster CTR
|
| 55 |
+
act = np.unique(np.concatenate([P_rows['u'].to_numpy(), cg['u'].to_numpy()]))
|
| 56 |
+
for nc in (64, 256):
|
| 57 |
+
km = MiniBatchKMeans(nc, random_state=0, batch_size=4096, n_init=3).fit(Un[act])
|
| 58 |
+
lab = km.predict(Un).astype(np.int32)
|
| 59 |
+
g = float(P_rows['clicked'].mean())
|
| 60 |
+
pr = P_rows.select('ii', 'clicked', pl.Series('cl', lab[P_rows['u'].to_numpy()]))
|
| 61 |
+
ict = pr.group_by('ii').agg(((pl.col('clicked').sum() + 20 * g) / (pl.len() + 20)).alias('ictr'))
|
| 62 |
+
cct = pr.group_by('cl', 'ii').agg(pl.col('clicked').sum().alias('c'), pl.len().alias('n'))
|
| 63 |
+
q = pl.DataFrame({'cl': lab[uu], 'ii': ii.astype(np.int32), 'r': np.arange(len(uu))}).join(cct, on=['cl', 'ii'], how='left') \
|
| 64 |
+
.join(ict, on='ii', how='left').sort('r').with_columns(pl.col('ictr').fill_null(g), pl.col('c').fill_null(0), pl.col('n').fill_null(0))
|
| 65 |
+
F[f'x_clctr{nc}'] = ((q['c'] + 10 * q['ictr']) / (q['n'] + 10)).to_numpy()
|
| 66 |
+
F[f'x_clctr{nc}_lift'] = F[f'x_clctr{nc}'] / q['ictr'].to_numpy()
|
| 67 |
+
F[f'x_climp{nc}'] = q['n'].to_numpy().astype(np.float32)
|
| 68 |
+
R = R.with_columns([pl.Series(c, v.astype(np.float32)) for c, v in F.items()])
|
| 69 |
+
zc = [c for c in F if not c.startswith('x_climp')]
|
| 70 |
+
R = R.with_columns([((pl.col(c) - pl.col(c).mean().over('eid')) / (pl.col(c).std().over('eid') + 1e-6)).alias(c + '_z') for c in zc] +
|
| 71 |
+
[pl.col(c).rank('average').over('eid').alias(c + '_rk') for c in zc])
|
| 72 |
+
return R.drop('eid', 'u', 'ii')
|
| 73 |
+
for name in sys.argv[1:]:
|
| 74 |
+
t0 = time.time(); C, W = SPEC[name]
|
| 75 |
+
if W == 'eval': R = build(C, EE, ER)
|
| 76 |
+
else:
|
| 77 |
+
T_ev = first_events(TE, C, W); R = build(C, T_ev, TR.join(T_ev.select('eid'), on='eid', how='semi'))
|
| 78 |
+
R.write_parquet(f'{OUT}/{name}.parquet'); print(name, R.shape, f'{time.time()-t0:.0f}s', flush=True)
|
parallel/src_snapshot/v2work/extra2.py
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Extra features #2 (leak-free; everything from ts < C):
|
| 2 |
+
- ridge 'taste vector' per user on frozen SVD k64 item vectors (clicks y=1 incl. pre-C click log, skipped impressions y=0 or -0.5)
|
| 3 |
+
- user state: time since last event/click, event-level click rate, recent-event click streaks, events in last 24h before C
|
| 4 |
+
Usage: python extra2.py NAME... -> v2work/feat_x2/NAME.parquet"""
|
| 5 |
+
import sys, os, time, numpy as np, polars as pl
|
| 6 |
+
from scipy import sparse
|
| 7 |
+
from sklearn.utils.extmath import randomized_svd
|
| 8 |
+
sys.path.insert(0, '/workspace/huawei/v3')
|
| 9 |
+
from features import load, click_log
|
| 10 |
+
from split import J20, DAY, first_events
|
| 11 |
+
OUT = '/workspace/huawei/v2work/feat_x2'; os.makedirs(OUT, exist_ok=True)
|
| 12 |
+
TE, TR, EE, ER, NI, NU = load('/workspace/huawei/v3/data')
|
| 13 |
+
SPEC = {**{f'f{k}': (J20 + k * DAY, DAY) for k in range(1, 7)}, 'val25': (J20 + 5 * DAY, None), 'val26': (J20 + 6 * DAY, None),
|
| 14 |
+
'val22': (J20 + 2 * DAY, None), 'eval': (J20 + 7 * DAY, 'eval'),
|
| 15 |
+
**{f'e{k}': (J20 + k * DAY, (5 - k) * DAY) for k in range(1, 5)}, **{f'E{k}': (J20 + k * DAY, (7 - k) * DAY) for k in range(1, 7)}}
|
| 16 |
+
def nrm(M): return M / (np.linalg.norm(M, axis=1, keepdims=True) + 1e-9)
|
| 17 |
+
def ridge_users(Wm, Ym, V, lam):
|
| 18 |
+
"""per-user solve (V_I^T W V_I + lam I) w = V_I^T (W*y). Wm, Ym: csr users x items (weights, weight*target)."""
|
| 19 |
+
k = V.shape[1]; A = np.zeros((Wm.shape[0], k, k), np.float32)
|
| 20 |
+
for a in range(k):
|
| 21 |
+
cols = Wm @ (V * V[:, a:a + 1]) # (NU, k): sum_i w_ui v_ia v_ib
|
| 22 |
+
A[:, a, :] = cols
|
| 23 |
+
A += lam * np.eye(k, dtype=np.float32)[None]
|
| 24 |
+
b = np.asarray(Ym @ V, dtype=np.float32)
|
| 25 |
+
return np.linalg.solve(A, b[:, :, None])[:, :, 0]
|
| 26 |
+
def build(C, T_ev, T_rows):
|
| 27 |
+
P_ev = TE.filter(pl.col('ts') < C)
|
| 28 |
+
P_rows = TR.join(P_ev.select('eid', 'u', 'ts'), on='eid', how='inner')
|
| 29 |
+
CL = click_log(C, P_ev, P_rows, T_ev) # user-level: train data + each target row's own history
|
| 30 |
+
CLg = click_log(C, P_ev, P_rows, T_ev.head(0)) # global/item-level: train data only
|
| 31 |
+
assert CL['ts'].max() < C and P_rows['ts'].max() < C
|
| 32 |
+
R = T_rows.join(T_ev.select('eid', 'u', 'ts'), on='eid', how='inner').select('rowid', 'eid', 'u', 'ii', 'ts')
|
| 33 |
+
uu = R['u'].to_numpy().astype(np.int64); ii = R['ii'].to_numpy().astype(np.int64)
|
| 34 |
+
# SVD exactly as v3 (tf-idf, row-normalised user x item click matrix)
|
| 35 |
+
cnt = CLg.group_by('u', 'ii').agg(pl.len().alias('n'))
|
| 36 |
+
Xc = sparse.csr_matrix((np.ones(cnt.height, np.float32), (cnt['u'].to_numpy(), cnt['ii'].to_numpy())), shape=(NU, NI))
|
| 37 |
+
df_ = np.asarray(Xc.sum(0)).ravel(); idf = np.log((NU + 1) / (df_ + 1)).astype(np.float32)
|
| 38 |
+
Xc = Xc @ sparse.diags(idf); Xc = sparse.diags(1 / (np.sqrt(np.asarray(Xc.multiply(Xc).sum(1)).ravel()) + 1e-6)) @ Xc
|
| 39 |
+
U, S, Vt = randomized_svd(Xc, 64, random_state=0, n_iter=5); Vn = nrm(Vt.T * np.sqrt(S)).astype(np.float32)
|
| 40 |
+
# (user,item) labels: clicked anywhere before C -> 1 ; shown-but-never-clicked -> 0
|
| 41 |
+
pos = CL.select('u', 'ii').unique().with_columns(pl.lit(1.0).alias('y'))
|
| 42 |
+
neg = P_rows.filter(pl.col('clicked') == 0).select('u', 'ii').unique().join(pos.select('u', 'ii'), on=['u', 'ii'], how='anti').with_columns(pl.lit(0.0).alias('y'))
|
| 43 |
+
L = pl.concat([pos, neg])
|
| 44 |
+
lu = L['u'].to_numpy(); li = L['ii'].to_numpy(); ly = L['y'].to_numpy().astype(np.float32)
|
| 45 |
+
Wm = sparse.csr_matrix((np.ones(len(ly), np.float32), (lu, li)), shape=(NU, NI))
|
| 46 |
+
cand = Vn[ii]; F = {}
|
| 47 |
+
for lam, yneg, nm in ((1.0, 0.0, 'l1'), (10.0, 0.0, 'l10'), (3.0, -0.5, 'l3n')):
|
| 48 |
+
yy = np.where(ly > 0, 1.0, yneg).astype(np.float32)
|
| 49 |
+
Ym = sparse.csr_matrix((yy, (lu, li)), shape=(NU, NI))
|
| 50 |
+
Wu = ridge_users(Wm, Ym, Vn, lam)
|
| 51 |
+
F[f'r_{nm}'] = (Wu[uu] * cand).sum(1)
|
| 52 |
+
# user state features
|
| 53 |
+
ue = P_ev.select('eid', 'u', 'ts').join(P_rows.group_by('eid').agg(pl.col('clicked').sum().alias('nc')), on='eid', how='left') \
|
| 54 |
+
.with_columns(pl.col('nc').fill_null(0)).sort('u', 'ts')
|
| 55 |
+
us = ue.group_by('u').agg(pl.col('ts').last().alias('last_ev'), (pl.col('nc') > 0).mean().alias('s_evrate'), pl.len().alias('s_nev'),
|
| 56 |
+
pl.col('nc').tail(1).sum().alias('s_clk_last1'), pl.col('nc').tail(3).sum().alias('s_clk_last3'),
|
| 57 |
+
(pl.col('nc').tail(5) > 0).sum().alias('s_evclk_last5'), (pl.col('ts') >= C - DAY).sum().alias('s_nev24h'),
|
| 58 |
+
pl.col('ts').filter(pl.col('nc') > 0).max().alias('last_clkev'))
|
| 59 |
+
lc = CL.group_by('u').agg(pl.col('ts').max().alias('last_click'), (pl.col('ts') >= C - DAY).sum().alias('s_clk24h'))
|
| 60 |
+
E = T_ev.select('eid', 'u', 'ts').join(us, on='u', how='left').join(lc, on='u', how='left').with_columns(
|
| 61 |
+
((pl.col('ts') - pl.col('last_ev')) / 3600).alias('s_since_ev_h'), ((pl.col('ts') - pl.col('last_click')) / 3600).alias('s_since_clk_h'),
|
| 62 |
+
((pl.col('ts') - pl.col('last_clkev')) / 3600).alias('s_since_clkev_h')).drop('u', 'ts', 'last_ev', 'last_click', 'last_clkev')
|
| 63 |
+
R = R.with_columns([pl.Series(c, v.astype(np.float32)) for c, v in F.items()])
|
| 64 |
+
R = R.with_columns([((pl.col(c) - pl.col(c).mean().over('eid')) / (pl.col(c).std().over('eid') + 1e-6)).alias(c + '_z') for c in F] +
|
| 65 |
+
[(pl.col(c) - pl.col(c).max().over('eid')).alias(c + '_dmax') for c in F])
|
| 66 |
+
R = R.join(E, on='eid', how='left')
|
| 67 |
+
return R.drop('eid', 'u', 'ii', 'ts')
|
| 68 |
+
for name in sys.argv[1:]:
|
| 69 |
+
t0 = time.time(); C, W = SPEC[name]
|
| 70 |
+
if W == 'eval': R = build(C, EE, ER)
|
| 71 |
+
else:
|
| 72 |
+
T_ev = first_events(TE, C, W); R = build(C, T_ev, TR.join(T_ev.select('eid'), on='eid', how='semi'))
|
| 73 |
+
R.write_parquet(f'{OUT}/{name}.parquet'); print(name, R.shape, f'{time.time()-t0:.0f}s', flush=True)
|
parallel/src_snapshot/v2work/feats.py
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Feature builder for TechArena CTR. All stats for a cutoff C use only events with ts < C
|
| 2 |
+
(plus the target events' own histories, which by construction contain only clicks < their ts)."""
|
| 3 |
+
import polars as pl, numpy as np, time
|
| 4 |
+
from scipy import sparse
|
| 5 |
+
from sklearn.utils.extmath import randomized_svd
|
| 6 |
+
|
| 7 |
+
DAY = 86400
|
| 8 |
+
K_SMOOTH = 20
|
| 9 |
+
|
| 10 |
+
def log(*a):
|
| 11 |
+
print(time.strftime('%H:%M:%S'), *a, flush=True)
|
| 12 |
+
|
| 13 |
+
def load(D):
|
| 14 |
+
TE = pl.read_parquet(f'{D}/train_events.parquet')
|
| 15 |
+
TR = pl.read_parquet(f'{D}/train_rows.parquet')
|
| 16 |
+
EE = pl.read_parquet(f'{D}/evaluation_events.parquet')
|
| 17 |
+
ER = pl.read_parquet(f'{D}/evaluation_rows.parquet')
|
| 18 |
+
# global item index
|
| 19 |
+
items = pl.concat([TR['item'], ER['item'], TE['h_items'].explode().drop_nulls(), EE['h_items'].explode().drop_nulls()]).unique().sort()
|
| 20 |
+
imap = pl.DataFrame({'item': items, 'ii': pl.arange(0, len(items), eager=True).cast(pl.Int32)})
|
| 21 |
+
def mapi(df, col='item'):
|
| 22 |
+
return df.join(imap.rename({'item': col, 'ii': col + '_ii'}), on=col, how='left')
|
| 23 |
+
TR = mapi(TR).drop('item').rename({'item_ii': 'ii'})
|
| 24 |
+
ER = mapi(ER).drop('item').rename({'item_ii': 'ii'})
|
| 25 |
+
def map_hist(E):
|
| 26 |
+
ex = E.select('eid', 'h_items').explode('h_items').with_row_index('r')
|
| 27 |
+
ex = ex.join(imap.rename({'item': 'h_items'}), on='h_items', how='left').sort('r')
|
| 28 |
+
h = ex.group_by('eid', maintain_order=True).agg(pl.col('ii').drop_nulls().alias('h'))
|
| 29 |
+
return E.drop('h_items').join(h, on='eid', how='left').with_columns(pl.col('h').fill_null([]))
|
| 30 |
+
TE = map_hist(TE); EE = map_hist(EE)
|
| 31 |
+
return TE, TR, EE, ER, len(items)
|
| 32 |
+
|
| 33 |
+
def first_events(TE, C, W):
|
| 34 |
+
"""first event per user in [C, C+W)"""
|
| 35 |
+
return (TE.filter((pl.col('ts') >= C) & (pl.col('ts') < C + W)).sort('ts')
|
| 36 |
+
.group_by('user', maintain_order=True).first())
|
| 37 |
+
|
| 38 |
+
def build(C, T_ev, T_rows, TE, TR, NI, svd_k=64, with_label=True):
|
| 39 |
+
t0 = time.time()
|
| 40 |
+
P_ev = TE.filter(pl.col('ts') < C)
|
| 41 |
+
P_rows = TR.join(P_ev.select('eid', 'user', 'ts', 'tb'), on='eid', how='inner')
|
| 42 |
+
g = P_rows['clicked'].mean() if len(P_rows) else 0.063
|
| 43 |
+
ndays = max((C - P_ev['ts'].min()) / DAY, 1e-3) if len(P_ev) else 1e-3
|
| 44 |
+
|
| 45 |
+
# ---------- click pool (user, item, ts) ----------
|
| 46 |
+
def hist_clicks(E):
|
| 47 |
+
return (E.select('user', 'h', 'h_ts').explode(['h', 'h_ts']).drop_nulls()
|
| 48 |
+
.rename({'h': 'ii', 'h_ts': 'ts'}))
|
| 49 |
+
pool = pl.concat([hist_clicks(P_ev), hist_clicks(T_ev),
|
| 50 |
+
P_rows.filter(pl.col('clicked') == 1).select('user', 'ii', 'ts')]).unique()
|
| 51 |
+
pool = pool.with_columns(pl.col('ii').cast(pl.Int32), pl.col('ts').cast(pl.Int64))
|
| 52 |
+
# global/item-level pool: train data only (no target-event histories, so nothing aggregates across eval rows);
|
| 53 |
+
# `pool` (with each target row's own history) is used only for that user's own features
|
| 54 |
+
poolg = pl.concat([hist_clicks(P_ev), P_rows.filter(pl.col('clicked') == 1).select('user', 'ii', 'ts')]).unique() \
|
| 55 |
+
.with_columns(pl.col('ii').cast(pl.Int32), pl.col('ts').cast(pl.Int64))
|
| 56 |
+
log('pool', len(pool))
|
| 57 |
+
|
| 58 |
+
# ---------- item stats ----------
|
| 59 |
+
it = P_rows.group_by('ii').agg(pl.len().alias('i_imp'), pl.col('clicked').sum().alias('i_clk'),
|
| 60 |
+
(pl.col('ts') >= C - DAY).sum().alias('i_imp1d'),
|
| 61 |
+
pl.col('clicked').filter(pl.col('ts') >= C - DAY).sum().alias('i_clk1d'))
|
| 62 |
+
it = it.with_columns(((pl.col('i_clk') + K_SMOOTH * g) / (pl.col('i_imp') + K_SMOOTH)).alias('i_ctr'),
|
| 63 |
+
((pl.col('i_clk1d') + K_SMOOTH * g) / (pl.col('i_imp1d') + K_SMOOTH)).alias('i_ctr1d'),
|
| 64 |
+
(pl.col('i_imp') / ndays).alias('i_imp_rate'))
|
| 65 |
+
ip = poolg.group_by('ii').agg(pl.len().alias('ip_n'), pl.col('user').n_unique().alias('ip_users'),
|
| 66 |
+
(pl.col('ts') >= C - DAY).sum().alias('ip_1d'),
|
| 67 |
+
(pl.col('ts') >= C - 3 * DAY).sum().alias('ip_3d'),
|
| 68 |
+
((pl.col('ts') - C) / DAY).exp().sum().alias('ip_dec1'),
|
| 69 |
+
((pl.col('ts') - C) / (3 * DAY)).exp().sum().alias('ip_dec3'),
|
| 70 |
+
(pl.col('ts') < 1737331200).sum().alias('ip_warm'))
|
| 71 |
+
itb = P_rows.group_by('ii', 'tb').agg(pl.len().alias('n'), pl.col('clicked').sum().alias('c'))
|
| 72 |
+
itb = itb.with_columns(((pl.col('c') + K_SMOOTH * g) / (pl.col('n') + K_SMOOTH)).alias('i_ctr_tb')).select('ii', 'tb', 'i_ctr_tb')
|
| 73 |
+
|
| 74 |
+
# ---------- user stats ----------
|
| 75 |
+
ue = P_rows.group_by('user').agg(pl.col('eid').n_unique().alias('u_ev'), pl.col('clicked').sum().alias('u_clk'),
|
| 76 |
+
pl.col('ts').max().alias('u_last_ev'))
|
| 77 |
+
ue = ue.with_columns(((pl.col('u_clk') + 5 * g * 10) / (pl.col('u_ev') + 5)).alias('u_ctr'))
|
| 78 |
+
up = pool.group_by('user').agg(pl.len().alias('up_n'), pl.col('ts').max().alias('up_last'),
|
| 79 |
+
pl.col('ii').n_unique().alias('up_uniq'),
|
| 80 |
+
(pl.col('ts') >= C - DAY).sum().alias('up_1d'))
|
| 81 |
+
# user-item prior impressions
|
| 82 |
+
ui = P_rows.group_by('user', 'ii').agg(pl.len().alias('ui_imp'), pl.col('clicked').sum().alias('ui_clk'),
|
| 83 |
+
pl.col('ts').max().alias('ui_last_imp'))
|
| 84 |
+
uip = pool.group_by('user', 'ii').agg(pl.len().alias('uip_n'), pl.col('ts').max().alias('uip_last'))
|
| 85 |
+
|
| 86 |
+
# ---------- co-occurrence (user sets) & transitions ----------
|
| 87 |
+
us = poolg.select('user', 'ii').unique()
|
| 88 |
+
ipop = us.group_by('ii').agg(pl.len().alias('pop'))
|
| 89 |
+
pairs = us.join(us, on='user', suffix='_b').filter(pl.col('ii') != pl.col('ii_b'))
|
| 90 |
+
co = pairs.group_by('ii', 'ii_b').agg(pl.len().alias('co'))
|
| 91 |
+
del pairs
|
| 92 |
+
co = (co.join(ipop, on='ii').join(ipop.rename({'ii': 'ii_b', 'pop': 'pop_b'}), on='ii_b')
|
| 93 |
+
.with_columns((pl.col('co') / (pl.col('pop') * pl.col('pop_b')).sqrt()).cast(pl.Float32).alias('cos'))
|
| 94 |
+
.select(pl.col('ii').cast(pl.Int32), pl.col('ii_b').cast(pl.Int32), pl.col('co').cast(pl.Int32), 'cos'))
|
| 95 |
+
log('cooc pairs', len(co))
|
| 96 |
+
seq = poolg.sort('user', 'ts')
|
| 97 |
+
tr_pairs = []
|
| 98 |
+
for lag in (1, 2, 3):
|
| 99 |
+
tr_pairs.append(seq.with_columns(pl.col('ii').shift(lag).over('user').alias('prev'))
|
| 100 |
+
.drop_nulls('prev').select(pl.col('prev').cast(pl.Int32), 'ii', pl.lit(1.0 / lag).alias('w')))
|
| 101 |
+
trn = pl.concat(tr_pairs).group_by('prev', 'ii').agg(pl.col('w').sum().alias('trn'))
|
| 102 |
+
trn_out = trn.group_by('prev').agg(pl.col('trn').sum().alias('trn_tot'))
|
| 103 |
+
trn = trn.join(trn_out, on='prev').with_columns((pl.col('trn') / (pl.col('trn_tot') + 5)).cast(pl.Float32).alias('trp')).select('prev', 'ii', 'trn', 'trp')
|
| 104 |
+
|
| 105 |
+
# ---------- SVD embeddings of user x item click matrix ----------
|
| 106 |
+
uidx = us.select('user').unique().with_row_index('ur')
|
| 107 |
+
m = us.join(uidx, on='user')
|
| 108 |
+
df_ = np.bincount(m['ii'].to_numpy(), minlength=NI)
|
| 109 |
+
idf = np.log((len(uidx) + 1) / (df_ + 1)).astype(np.float32)
|
| 110 |
+
X = sparse.csr_matrix((idf[m['ii'].to_numpy()], (m['ur'].to_numpy(), m['ii'].to_numpy())), shape=(len(uidx), NI))
|
| 111 |
+
rn = np.sqrt(np.asarray(X.multiply(X).sum(1)).ravel()) + 1e-6
|
| 112 |
+
X = sparse.diags(1 / rn) @ X
|
| 113 |
+
U, S, Vt = randomized_svd(X, svd_k, random_state=0, n_iter=4)
|
| 114 |
+
V = Vt.T.astype(np.float32) # unscaled singular vectors (best in emb_exp)
|
| 115 |
+
V /= (np.linalg.norm(V, axis=1, keepdims=True) + 1e-6)
|
| 116 |
+
V32 = V[:, :32] / (np.linalg.norm(V[:, :32], axis=1, keepdims=True) + 1e-6)
|
| 117 |
+
# fold-in of every target user's own pool (train clicks + own history) onto the train-only factors
|
| 118 |
+
uidx = pool.select('user').unique().with_row_index('ur')
|
| 119 |
+
mu = pool.select('user', 'ii').unique().join(uidx, on='user')
|
| 120 |
+
Xu = sparse.csr_matrix((idf[mu['ii'].to_numpy()], (mu['ur'].to_numpy(), mu['ii'].to_numpy())), shape=(len(uidx), NI))
|
| 121 |
+
Xu = sparse.diags(1 / (np.sqrt(np.asarray(Xu.multiply(Xu).sum(1)).ravel()) + 1e-6)) @ Xu
|
| 122 |
+
U = np.asarray(Xu @ (Vt.T / S)).astype(np.float32)
|
| 123 |
+
S = np.ones_like(S)
|
| 124 |
+
log('svd done')
|
| 125 |
+
|
| 126 |
+
# ---------- assemble target rows ----------
|
| 127 |
+
R = T_rows.join(T_ev, on='eid', how='left').with_row_index('r')
|
| 128 |
+
R = R.with_columns((pl.col('ts') - C).alias('stale'), ((pl.col('ts') // 3600) % 24).cast(pl.Int8).alias('hour'))
|
| 129 |
+
R = (R.join(it, on='ii', how='left').join(ip, on='ii', how='left').join(itb, on=['ii', 'tb'], how='left')
|
| 130 |
+
.join(ue, on='user', how='left').join(up, on='user', how='left')
|
| 131 |
+
.join(ui, on=['user', 'ii'], how='left').join(uip, on=['user', 'ii'], how='left').sort('r'))
|
| 132 |
+
R = R.with_columns(
|
| 133 |
+
pl.col('h').list.len().alias('h_len'),
|
| 134 |
+
(pl.col('ts') - pl.col('h_ts').list.last()).alias('h_since_last'),
|
| 135 |
+
(pl.col('ts') - pl.col('h_ts').list.first()).alias('h_since_first'),
|
| 136 |
+
pl.col('h').list.n_unique().alias('h_uniq'),
|
| 137 |
+
pl.col('h_ts').list.eval((pl.element() >= pl.element().max() - DAY).sum()).list.first().alias('h_1d_before_last'),
|
| 138 |
+
(pl.col('ts') - pl.col('u_last_ev')).alias('u_since_ev'),
|
| 139 |
+
(pl.col('ts') - pl.col('up_last')).alias('up_since'),
|
| 140 |
+
(pl.col('ts') - pl.col('ui_last_imp')).alias('ui_since_imp'),
|
| 141 |
+
(pl.col('ts') - pl.col('uip_last')).alias('uip_since'),
|
| 142 |
+
(pl.col('ui_imp') - pl.col('ui_clk')).alias('ui_noclk'),
|
| 143 |
+
(pl.col('ip_n') / (pl.col('i_imp') + 1)).alias('ip_per_imp'),
|
| 144 |
+
(pl.col('i_ctr_tb') - pl.col('i_ctr')).alias('i_ctr_tb_diff'),
|
| 145 |
+
)
|
| 146 |
+
R = R.with_columns((pl.col('h_since_first') / (pl.col('h_len') - 1).clip(1)).alias('h_gap_mean'),
|
| 147 |
+
(pl.col('h_uniq') / pl.col('h_len').clip(1)).alias('h_uniq_ratio'))
|
| 148 |
+
|
| 149 |
+
# pairwise candidate x history
|
| 150 |
+
PR = (R.select('r', 'ii', 'h', 'h_ts', 'ts').with_columns(pl.int_ranges(pl.col('h').list.len(), 0, -1).alias('pos'))
|
| 151 |
+
.explode(['h', 'h_ts', 'pos']).drop_nulls('h')
|
| 152 |
+
.with_columns(pl.col('h').cast(pl.Int32)))
|
| 153 |
+
PR = PR.join(co.rename({'ii': 'ii', 'ii_b': 'h'}), on=['ii', 'h'], how='left').with_columns(pl.col('cos').fill_null(0), pl.col('co').fill_null(0))
|
| 154 |
+
PR = PR.with_columns((0.85 ** (pl.col('pos') - 1)).alias('rw'), (pl.col('h') == pl.col('ii')).alias('same'))
|
| 155 |
+
agg = PR.group_by('r').agg(
|
| 156 |
+
pl.col('cos').sum().alias('co_sum'), pl.col('cos').max().alias('co_max'), pl.col('cos').mean().alias('co_mean'),
|
| 157 |
+
(pl.col('cos') * pl.col('rw')).sum().alias('co_rw'), pl.col('co').sum().alias('co_cnt'),
|
| 158 |
+
pl.col('cos').filter(pl.col('pos') <= 3).sum().alias('co_last3'),
|
| 159 |
+
pl.col('same').sum().alias('inh_cnt'), pl.col('pos').filter(pl.col('same')).min().alias('inh_pos'),
|
| 160 |
+
(pl.col('ts') - pl.col('h_ts')).filter(pl.col('same')).min().alias('inh_since'),
|
| 161 |
+
(pl.col('cos') > 0).sum().alias('co_nz'))
|
| 162 |
+
R = R.join(agg, on='r', how='left')
|
| 163 |
+
# transitions from last 3 history items
|
| 164 |
+
TRp = (R.select('r', 'ii', pl.col('h').list.tail(3).alias('h3')).with_columns(pl.int_ranges(pl.col('h3').list.len(), 0, -1).alias('pos'))
|
| 165 |
+
.explode(['h3', 'pos']).drop_nulls('h3').rename({'h3': 'prev'}).with_columns(pl.col('prev').cast(pl.Int32))
|
| 166 |
+
.join(trn, on=['prev', 'ii'], how='left').with_columns(pl.col('trn').fill_null(0), pl.col('trp').fill_null(0)))
|
| 167 |
+
tagg = TRp.group_by('r').agg(pl.col('trn').filter(pl.col('pos') == 1).sum().alias('tr_last'),
|
| 168 |
+
pl.col('trp').filter(pl.col('pos') == 1).sum().alias('trp_last'),
|
| 169 |
+
pl.col('trn').sum().alias('tr_last3'), pl.col('trp').sum().alias('trp_last3'))
|
| 170 |
+
R = R.join(tagg, on='r', how='left').sort('r')
|
| 171 |
+
# SVD cosine: recency weighted mean of history vectors
|
| 172 |
+
hv = np.zeros((len(R), V.shape[1]), np.float32)
|
| 173 |
+
ex = PR.select('r', 'h', 'rw')
|
| 174 |
+
np.add.at(hv, ex['r'].to_numpy(), V[ex['h'].to_numpy()] * ex['rw'].to_numpy()[:, None].astype(np.float32))
|
| 175 |
+
hvn = hv / (np.linalg.norm(hv, axis=1, keepdims=True) + 1e-6)
|
| 176 |
+
cand = V[R['ii'].to_numpy()]
|
| 177 |
+
svd_cos = (hvn * cand).sum(1)
|
| 178 |
+
# user factor from SVD (pool-based, covers clicks beyond last 20)
|
| 179 |
+
uf = np.zeros((len(R), V.shape[1]), np.float32)
|
| 180 |
+
um = R.select('user').join(uidx, on='user', how='left')['ur'].to_numpy()
|
| 181 |
+
ok = ~np.isnan(um.astype(float)) if um.dtype.kind == 'f' else np.ones(len(um), bool)
|
| 182 |
+
umi = np.nan_to_num(um.astype(float), nan=0).astype(np.int64)
|
| 183 |
+
ufv = (U[umi] * np.sqrt(S)).astype(np.float32); ufv[~ok] = 0
|
| 184 |
+
ufv /= (np.linalg.norm(ufv, axis=1, keepdims=True) + 1e-6)
|
| 185 |
+
svd_ucos = (ufv * cand).sum(1)
|
| 186 |
+
hv32 = np.zeros((len(R), 32), np.float32)
|
| 187 |
+
np.add.at(hv32, ex['r'].to_numpy(), V32[ex['h'].to_numpy()] * ex['rw'].to_numpy()[:, None].astype(np.float32))
|
| 188 |
+
svd32_cos = (hv32 / (np.linalg.norm(hv32, axis=1, keepdims=True) + 1e-6) * V32[R['ii'].to_numpy()]).sum(1)
|
| 189 |
+
extra_svd = {}
|
| 190 |
+
for kk in (16, 24, 48):
|
| 191 |
+
Vk = V[:, :kk] / (np.linalg.norm(V[:, :kk], axis=1, keepdims=True) + 1e-6)
|
| 192 |
+
hk = np.zeros((len(R), kk), np.float32)
|
| 193 |
+
np.add.at(hk, ex['r'].to_numpy(), Vk[ex['h'].to_numpy()] * ex['rw'].to_numpy()[:, None].astype(np.float32))
|
| 194 |
+
extra_svd[f'svd{kk}_cos'] = (hk / (np.linalg.norm(hk, axis=1, keepdims=True) + 1e-6) * Vk[R['ii'].to_numpy()]).sum(1)
|
| 195 |
+
# pairwise svd similarity: max over history, and with the most recent item
|
| 196 |
+
pr_r = PR['r'].to_numpy(); pr_c = PR['ii'].to_numpy(); pr_h = PR['h'].to_numpy()
|
| 197 |
+
sims = np.empty(len(PR), np.float32)
|
| 198 |
+
for k in range(0, len(PR), 2_000_000):
|
| 199 |
+
sl = slice(k, k + 2_000_000); sims[sl] = (V[pr_c[sl]] * V[pr_h[sl]]).sum(1)
|
| 200 |
+
sp = pl.DataFrame({'r': pr_r, 'sim': sims, 'pos': PR['pos'].to_numpy(), 'same': PR['same'].to_numpy()}).filter(~pl.col('same'))
|
| 201 |
+
sagg = sp.group_by('r').agg(pl.col('sim').max().alias('svd_maxsim'), pl.col('sim').filter(pl.col('pos') == 1).max().alias('svd_last'),
|
| 202 |
+
pl.col('sim').top_k(3).mean().alias('svd_top3'))
|
| 203 |
+
R = R.with_columns(pl.Series('svd_cos', svd_cos), pl.Series('svd_ucos', svd_ucos), pl.Series('svd_hnorm', np.linalg.norm(hv, axis=1)),
|
| 204 |
+
pl.Series('svd32_cos', svd32_cos), *[pl.Series(k, v) for k, v in extra_svd.items()])
|
| 205 |
+
R = R.join(sagg, on='r', how='left').sort('r')
|
| 206 |
+
|
| 207 |
+
fill0 = ['i_imp', 'i_clk', 'i_imp1d', 'i_clk1d', 'ip_n', 'ip_users', 'ip_1d', 'ip_3d', 'ip_dec1', 'ip_dec3', 'ip_warm',
|
| 208 |
+
'u_ev', 'u_clk', 'up_n', 'up_uniq', 'up_1d', 'ui_imp', 'ui_clk', 'ui_noclk', 'uip_n', 'co_sum', 'co_max', 'co_mean', 'co_rw',
|
| 209 |
+
'co_cnt', 'co_last3', 'inh_cnt', 'co_nz', 'tr_last', 'trp_last', 'tr_last3', 'trp_last3']
|
| 210 |
+
R = R.with_columns([pl.col(c).fill_null(0) for c in fill0])
|
| 211 |
+
R = R.with_columns(pl.col('i_ctr').fill_null(g), pl.col('i_ctr1d').fill_null(g), pl.col('i_ctr_tb').fill_null(g), pl.col('u_ctr').fill_null(g * 10))
|
| 212 |
+
# within-event relative features
|
| 213 |
+
rel = ['i_ctr', 'ip_dec1', 'ip_n', 'co_sum', 'co_rw', 'co_max', 'svd_cos', 'svd_ucos', 'svd32_cos', 'svd16_cos', 'svd24_cos', 'svd48_cos', 'svd_maxsim', 'svd_top3', 'tr_last3', 'uip_n', 'i_ctr_tb', 'ui_noclk']
|
| 214 |
+
R = R.with_columns([pl.col(c).rank('average').over('eid').alias(c + '_rk') for c in rel] +
|
| 215 |
+
[(pl.col(c) - pl.col(c).mean().over('eid')).alias(c + '_dm') for c in rel] +
|
| 216 |
+
[(pl.col(c) / (pl.col(c).max().over('eid') + 1e-9)).alias(c + '_rmax') for c in ['co_sum', 'svd_cos', 'ip_dec1', 'co_rw']])
|
| 217 |
+
R = R.with_columns(pl.col('co_rw').max().over('eid').alias('ev_co_max'), pl.col('svd_cos').max().over('eid').alias('ev_svd_max'),
|
| 218 |
+
pl.col('svd_cos').mean().over('eid').alias('ev_svd_mean'),
|
| 219 |
+
(pl.col('inh_cnt') > 0).sum().over('eid').alias('ev_n_inh'), pl.col('i_ctr').mean().over('eid').alias('ev_ictr_mean'),
|
| 220 |
+
pl.col('ip_dec1').mean().over('eid').alias('ev_ipdec_mean'))
|
| 221 |
+
log(f'build C={C} rows={len(R)} {time.time()-t0:.0f}s')
|
| 222 |
+
starter = dict(g=g)
|
| 223 |
+
return R
|
| 224 |
+
|
| 225 |
+
DROP = ['r', 'rowid', 'eid', 'user', 'ii', 'h', 'h_ts', 'ts', 'clicked', 'u_last_ev', 'up_last', 'ui_last_imp', 'uip_last', 'fold']
|
parallel/src_snapshot/v2work/feats_pre_noeval.py
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Feature builder for TechArena CTR. All stats for a cutoff C use only events with ts < C
|
| 2 |
+
(plus the target events' own histories, which by construction contain only clicks < their ts)."""
|
| 3 |
+
import polars as pl, numpy as np, time
|
| 4 |
+
from scipy import sparse
|
| 5 |
+
from sklearn.utils.extmath import randomized_svd
|
| 6 |
+
|
| 7 |
+
DAY = 86400
|
| 8 |
+
K_SMOOTH = 20
|
| 9 |
+
|
| 10 |
+
def log(*a):
|
| 11 |
+
print(time.strftime('%H:%M:%S'), *a, flush=True)
|
| 12 |
+
|
| 13 |
+
def load(D):
|
| 14 |
+
TE = pl.read_parquet(f'{D}/train_events.parquet')
|
| 15 |
+
TR = pl.read_parquet(f'{D}/train_rows.parquet')
|
| 16 |
+
EE = pl.read_parquet(f'{D}/evaluation_events.parquet')
|
| 17 |
+
ER = pl.read_parquet(f'{D}/evaluation_rows.parquet')
|
| 18 |
+
# global item index
|
| 19 |
+
items = pl.concat([TR['item'], ER['item'], TE['h_items'].explode().drop_nulls(), EE['h_items'].explode().drop_nulls()]).unique().sort()
|
| 20 |
+
imap = pl.DataFrame({'item': items, 'ii': pl.arange(0, len(items), eager=True).cast(pl.Int32)})
|
| 21 |
+
def mapi(df, col='item'):
|
| 22 |
+
return df.join(imap.rename({'item': col, 'ii': col + '_ii'}), on=col, how='left')
|
| 23 |
+
TR = mapi(TR).drop('item').rename({'item_ii': 'ii'})
|
| 24 |
+
ER = mapi(ER).drop('item').rename({'item_ii': 'ii'})
|
| 25 |
+
def map_hist(E):
|
| 26 |
+
ex = E.select('eid', 'h_items').explode('h_items').with_row_index('r')
|
| 27 |
+
ex = ex.join(imap.rename({'item': 'h_items'}), on='h_items', how='left').sort('r')
|
| 28 |
+
h = ex.group_by('eid', maintain_order=True).agg(pl.col('ii').drop_nulls().alias('h'))
|
| 29 |
+
return E.drop('h_items').join(h, on='eid', how='left').with_columns(pl.col('h').fill_null([]))
|
| 30 |
+
TE = map_hist(TE); EE = map_hist(EE)
|
| 31 |
+
return TE, TR, EE, ER, len(items)
|
| 32 |
+
|
| 33 |
+
def first_events(TE, C, W):
|
| 34 |
+
"""first event per user in [C, C+W)"""
|
| 35 |
+
return (TE.filter((pl.col('ts') >= C) & (pl.col('ts') < C + W)).sort('ts')
|
| 36 |
+
.group_by('user', maintain_order=True).first())
|
| 37 |
+
|
| 38 |
+
def build(C, T_ev, T_rows, TE, TR, NI, svd_k=64, with_label=True):
|
| 39 |
+
t0 = time.time()
|
| 40 |
+
P_ev = TE.filter(pl.col('ts') < C)
|
| 41 |
+
P_rows = TR.join(P_ev.select('eid', 'user', 'ts', 'tb'), on='eid', how='inner')
|
| 42 |
+
g = P_rows['clicked'].mean() if len(P_rows) else 0.063
|
| 43 |
+
ndays = max((C - P_ev['ts'].min()) / DAY, 1e-3) if len(P_ev) else 1e-3
|
| 44 |
+
|
| 45 |
+
# ---------- click pool (user, item, ts) ----------
|
| 46 |
+
def hist_clicks(E):
|
| 47 |
+
return (E.select('user', 'h', 'h_ts').explode(['h', 'h_ts']).drop_nulls()
|
| 48 |
+
.rename({'h': 'ii', 'h_ts': 'ts'}))
|
| 49 |
+
pool = pl.concat([hist_clicks(P_ev), hist_clicks(T_ev),
|
| 50 |
+
P_rows.filter(pl.col('clicked') == 1).select('user', 'ii', 'ts')]).unique()
|
| 51 |
+
pool = pool.with_columns(pl.col('ii').cast(pl.Int32), pl.col('ts').cast(pl.Int64))
|
| 52 |
+
log('pool', len(pool))
|
| 53 |
+
|
| 54 |
+
# ---------- item stats ----------
|
| 55 |
+
it = P_rows.group_by('ii').agg(pl.len().alias('i_imp'), pl.col('clicked').sum().alias('i_clk'),
|
| 56 |
+
(pl.col('ts') >= C - DAY).sum().alias('i_imp1d'),
|
| 57 |
+
pl.col('clicked').filter(pl.col('ts') >= C - DAY).sum().alias('i_clk1d'))
|
| 58 |
+
it = it.with_columns(((pl.col('i_clk') + K_SMOOTH * g) / (pl.col('i_imp') + K_SMOOTH)).alias('i_ctr'),
|
| 59 |
+
((pl.col('i_clk1d') + K_SMOOTH * g) / (pl.col('i_imp1d') + K_SMOOTH)).alias('i_ctr1d'),
|
| 60 |
+
(pl.col('i_imp') / ndays).alias('i_imp_rate'))
|
| 61 |
+
ip = pool.group_by('ii').agg(pl.len().alias('ip_n'), pl.col('user').n_unique().alias('ip_users'),
|
| 62 |
+
(pl.col('ts') >= C - DAY).sum().alias('ip_1d'),
|
| 63 |
+
(pl.col('ts') >= C - 3 * DAY).sum().alias('ip_3d'),
|
| 64 |
+
((pl.col('ts') - C) / DAY).exp().sum().alias('ip_dec1'),
|
| 65 |
+
((pl.col('ts') - C) / (3 * DAY)).exp().sum().alias('ip_dec3'),
|
| 66 |
+
(pl.col('ts') < 1737331200).sum().alias('ip_warm'))
|
| 67 |
+
itb = P_rows.group_by('ii', 'tb').agg(pl.len().alias('n'), pl.col('clicked').sum().alias('c'))
|
| 68 |
+
itb = itb.with_columns(((pl.col('c') + K_SMOOTH * g) / (pl.col('n') + K_SMOOTH)).alias('i_ctr_tb')).select('ii', 'tb', 'i_ctr_tb')
|
| 69 |
+
|
| 70 |
+
# ---------- user stats ----------
|
| 71 |
+
ue = P_rows.group_by('user').agg(pl.col('eid').n_unique().alias('u_ev'), pl.col('clicked').sum().alias('u_clk'),
|
| 72 |
+
pl.col('ts').max().alias('u_last_ev'))
|
| 73 |
+
ue = ue.with_columns(((pl.col('u_clk') + 5 * g * 10) / (pl.col('u_ev') + 5)).alias('u_ctr'))
|
| 74 |
+
up = pool.group_by('user').agg(pl.len().alias('up_n'), pl.col('ts').max().alias('up_last'),
|
| 75 |
+
pl.col('ii').n_unique().alias('up_uniq'),
|
| 76 |
+
(pl.col('ts') >= C - DAY).sum().alias('up_1d'))
|
| 77 |
+
# user-item prior impressions
|
| 78 |
+
ui = P_rows.group_by('user', 'ii').agg(pl.len().alias('ui_imp'), pl.col('clicked').sum().alias('ui_clk'),
|
| 79 |
+
pl.col('ts').max().alias('ui_last_imp'))
|
| 80 |
+
uip = pool.group_by('user', 'ii').agg(pl.len().alias('uip_n'), pl.col('ts').max().alias('uip_last'))
|
| 81 |
+
|
| 82 |
+
# ---------- co-occurrence (user sets) & transitions ----------
|
| 83 |
+
us = pool.select('user', 'ii').unique()
|
| 84 |
+
ipop = us.group_by('ii').agg(pl.len().alias('pop'))
|
| 85 |
+
pairs = us.join(us, on='user', suffix='_b').filter(pl.col('ii') != pl.col('ii_b'))
|
| 86 |
+
co = pairs.group_by('ii', 'ii_b').agg(pl.len().alias('co'))
|
| 87 |
+
del pairs
|
| 88 |
+
co = (co.join(ipop, on='ii').join(ipop.rename({'ii': 'ii_b', 'pop': 'pop_b'}), on='ii_b')
|
| 89 |
+
.with_columns((pl.col('co') / (pl.col('pop') * pl.col('pop_b')).sqrt()).cast(pl.Float32).alias('cos'))
|
| 90 |
+
.select(pl.col('ii').cast(pl.Int32), pl.col('ii_b').cast(pl.Int32), pl.col('co').cast(pl.Int32), 'cos'))
|
| 91 |
+
log('cooc pairs', len(co))
|
| 92 |
+
seq = pool.sort('user', 'ts')
|
| 93 |
+
tr_pairs = []
|
| 94 |
+
for lag in (1, 2, 3):
|
| 95 |
+
tr_pairs.append(seq.with_columns(pl.col('ii').shift(lag).over('user').alias('prev'))
|
| 96 |
+
.drop_nulls('prev').select(pl.col('prev').cast(pl.Int32), 'ii', pl.lit(1.0 / lag).alias('w')))
|
| 97 |
+
trn = pl.concat(tr_pairs).group_by('prev', 'ii').agg(pl.col('w').sum().alias('trn'))
|
| 98 |
+
trn_out = trn.group_by('prev').agg(pl.col('trn').sum().alias('trn_tot'))
|
| 99 |
+
trn = trn.join(trn_out, on='prev').with_columns((pl.col('trn') / (pl.col('trn_tot') + 5)).cast(pl.Float32).alias('trp')).select('prev', 'ii', 'trn', 'trp')
|
| 100 |
+
|
| 101 |
+
# ---------- SVD embeddings of user x item click matrix ----------
|
| 102 |
+
uidx = us.select('user').unique().with_row_index('ur')
|
| 103 |
+
m = us.join(uidx, on='user')
|
| 104 |
+
df_ = np.bincount(m['ii'].to_numpy(), minlength=NI)
|
| 105 |
+
idf = np.log((len(uidx) + 1) / (df_ + 1)).astype(np.float32)
|
| 106 |
+
X = sparse.csr_matrix((idf[m['ii'].to_numpy()], (m['ur'].to_numpy(), m['ii'].to_numpy())), shape=(len(uidx), NI))
|
| 107 |
+
rn = np.sqrt(np.asarray(X.multiply(X).sum(1)).ravel()) + 1e-6
|
| 108 |
+
X = sparse.diags(1 / rn) @ X
|
| 109 |
+
U, S, Vt = randomized_svd(X, svd_k, random_state=0, n_iter=4)
|
| 110 |
+
V = Vt.T.astype(np.float32) # unscaled singular vectors (best in emb_exp)
|
| 111 |
+
V /= (np.linalg.norm(V, axis=1, keepdims=True) + 1e-6)
|
| 112 |
+
V32 = V[:, :32] / (np.linalg.norm(V[:, :32], axis=1, keepdims=True) + 1e-6)
|
| 113 |
+
S = np.ones_like(S)
|
| 114 |
+
log('svd done')
|
| 115 |
+
|
| 116 |
+
# ---------- assemble target rows ----------
|
| 117 |
+
R = T_rows.join(T_ev, on='eid', how='left').with_row_index('r')
|
| 118 |
+
R = R.with_columns((pl.col('ts') - C).alias('stale'), ((pl.col('ts') // 3600) % 24).cast(pl.Int8).alias('hour'))
|
| 119 |
+
R = (R.join(it, on='ii', how='left').join(ip, on='ii', how='left').join(itb, on=['ii', 'tb'], how='left')
|
| 120 |
+
.join(ue, on='user', how='left').join(up, on='user', how='left')
|
| 121 |
+
.join(ui, on=['user', 'ii'], how='left').join(uip, on=['user', 'ii'], how='left').sort('r'))
|
| 122 |
+
R = R.with_columns(
|
| 123 |
+
pl.col('h').list.len().alias('h_len'),
|
| 124 |
+
(pl.col('ts') - pl.col('h_ts').list.last()).alias('h_since_last'),
|
| 125 |
+
(pl.col('ts') - pl.col('h_ts').list.first()).alias('h_since_first'),
|
| 126 |
+
pl.col('h').list.n_unique().alias('h_uniq'),
|
| 127 |
+
pl.col('h_ts').list.eval((pl.element() >= pl.element().max() - DAY).sum()).list.first().alias('h_1d_before_last'),
|
| 128 |
+
(pl.col('ts') - pl.col('u_last_ev')).alias('u_since_ev'),
|
| 129 |
+
(pl.col('ts') - pl.col('up_last')).alias('up_since'),
|
| 130 |
+
(pl.col('ts') - pl.col('ui_last_imp')).alias('ui_since_imp'),
|
| 131 |
+
(pl.col('ts') - pl.col('uip_last')).alias('uip_since'),
|
| 132 |
+
(pl.col('ui_imp') - pl.col('ui_clk')).alias('ui_noclk'),
|
| 133 |
+
(pl.col('ip_n') / (pl.col('i_imp') + 1)).alias('ip_per_imp'),
|
| 134 |
+
(pl.col('i_ctr_tb') - pl.col('i_ctr')).alias('i_ctr_tb_diff'),
|
| 135 |
+
)
|
| 136 |
+
R = R.with_columns((pl.col('h_since_first') / (pl.col('h_len') - 1).clip(1)).alias('h_gap_mean'),
|
| 137 |
+
(pl.col('h_uniq') / pl.col('h_len').clip(1)).alias('h_uniq_ratio'))
|
| 138 |
+
|
| 139 |
+
# pairwise candidate x history
|
| 140 |
+
PR = (R.select('r', 'ii', 'h', 'h_ts', 'ts').with_columns(pl.int_ranges(pl.col('h').list.len(), 0, -1).alias('pos'))
|
| 141 |
+
.explode(['h', 'h_ts', 'pos']).drop_nulls('h')
|
| 142 |
+
.with_columns(pl.col('h').cast(pl.Int32)))
|
| 143 |
+
PR = PR.join(co.rename({'ii': 'ii', 'ii_b': 'h'}), on=['ii', 'h'], how='left').with_columns(pl.col('cos').fill_null(0), pl.col('co').fill_null(0))
|
| 144 |
+
PR = PR.with_columns((0.85 ** (pl.col('pos') - 1)).alias('rw'), (pl.col('h') == pl.col('ii')).alias('same'))
|
| 145 |
+
agg = PR.group_by('r').agg(
|
| 146 |
+
pl.col('cos').sum().alias('co_sum'), pl.col('cos').max().alias('co_max'), pl.col('cos').mean().alias('co_mean'),
|
| 147 |
+
(pl.col('cos') * pl.col('rw')).sum().alias('co_rw'), pl.col('co').sum().alias('co_cnt'),
|
| 148 |
+
pl.col('cos').filter(pl.col('pos') <= 3).sum().alias('co_last3'),
|
| 149 |
+
pl.col('same').sum().alias('inh_cnt'), pl.col('pos').filter(pl.col('same')).min().alias('inh_pos'),
|
| 150 |
+
(pl.col('ts') - pl.col('h_ts')).filter(pl.col('same')).min().alias('inh_since'),
|
| 151 |
+
(pl.col('cos') > 0).sum().alias('co_nz'))
|
| 152 |
+
R = R.join(agg, on='r', how='left')
|
| 153 |
+
# transitions from last 3 history items
|
| 154 |
+
TRp = (R.select('r', 'ii', pl.col('h').list.tail(3).alias('h3')).with_columns(pl.int_ranges(pl.col('h3').list.len(), 0, -1).alias('pos'))
|
| 155 |
+
.explode(['h3', 'pos']).drop_nulls('h3').rename({'h3': 'prev'}).with_columns(pl.col('prev').cast(pl.Int32))
|
| 156 |
+
.join(trn, on=['prev', 'ii'], how='left').with_columns(pl.col('trn').fill_null(0), pl.col('trp').fill_null(0)))
|
| 157 |
+
tagg = TRp.group_by('r').agg(pl.col('trn').filter(pl.col('pos') == 1).sum().alias('tr_last'),
|
| 158 |
+
pl.col('trp').filter(pl.col('pos') == 1).sum().alias('trp_last'),
|
| 159 |
+
pl.col('trn').sum().alias('tr_last3'), pl.col('trp').sum().alias('trp_last3'))
|
| 160 |
+
R = R.join(tagg, on='r', how='left').sort('r')
|
| 161 |
+
# SVD cosine: recency weighted mean of history vectors
|
| 162 |
+
hv = np.zeros((len(R), V.shape[1]), np.float32)
|
| 163 |
+
ex = PR.select('r', 'h', 'rw')
|
| 164 |
+
np.add.at(hv, ex['r'].to_numpy(), V[ex['h'].to_numpy()] * ex['rw'].to_numpy()[:, None].astype(np.float32))
|
| 165 |
+
hvn = hv / (np.linalg.norm(hv, axis=1, keepdims=True) + 1e-6)
|
| 166 |
+
cand = V[R['ii'].to_numpy()]
|
| 167 |
+
svd_cos = (hvn * cand).sum(1)
|
| 168 |
+
# user factor from SVD (pool-based, covers clicks beyond last 20)
|
| 169 |
+
uf = np.zeros((len(R), V.shape[1]), np.float32)
|
| 170 |
+
um = R.select('user').join(uidx, on='user', how='left')['ur'].to_numpy()
|
| 171 |
+
ok = ~np.isnan(um.astype(float)) if um.dtype.kind == 'f' else np.ones(len(um), bool)
|
| 172 |
+
umi = np.nan_to_num(um.astype(float), nan=0).astype(np.int64)
|
| 173 |
+
ufv = (U[umi] * np.sqrt(S)).astype(np.float32); ufv[~ok] = 0
|
| 174 |
+
ufv /= (np.linalg.norm(ufv, axis=1, keepdims=True) + 1e-6)
|
| 175 |
+
svd_ucos = (ufv * cand).sum(1)
|
| 176 |
+
hv32 = np.zeros((len(R), 32), np.float32)
|
| 177 |
+
np.add.at(hv32, ex['r'].to_numpy(), V32[ex['h'].to_numpy()] * ex['rw'].to_numpy()[:, None].astype(np.float32))
|
| 178 |
+
svd32_cos = (hv32 / (np.linalg.norm(hv32, axis=1, keepdims=True) + 1e-6) * V32[R['ii'].to_numpy()]).sum(1)
|
| 179 |
+
extra_svd = {}
|
| 180 |
+
for kk in (16, 24, 48):
|
| 181 |
+
Vk = V[:, :kk] / (np.linalg.norm(V[:, :kk], axis=1, keepdims=True) + 1e-6)
|
| 182 |
+
hk = np.zeros((len(R), kk), np.float32)
|
| 183 |
+
np.add.at(hk, ex['r'].to_numpy(), Vk[ex['h'].to_numpy()] * ex['rw'].to_numpy()[:, None].astype(np.float32))
|
| 184 |
+
extra_svd[f'svd{kk}_cos'] = (hk / (np.linalg.norm(hk, axis=1, keepdims=True) + 1e-6) * Vk[R['ii'].to_numpy()]).sum(1)
|
| 185 |
+
# pairwise svd similarity: max over history, and with the most recent item
|
| 186 |
+
pr_r = PR['r'].to_numpy(); pr_c = PR['ii'].to_numpy(); pr_h = PR['h'].to_numpy()
|
| 187 |
+
sims = np.empty(len(PR), np.float32)
|
| 188 |
+
for k in range(0, len(PR), 2_000_000):
|
| 189 |
+
sl = slice(k, k + 2_000_000); sims[sl] = (V[pr_c[sl]] * V[pr_h[sl]]).sum(1)
|
| 190 |
+
sp = pl.DataFrame({'r': pr_r, 'sim': sims, 'pos': PR['pos'].to_numpy(), 'same': PR['same'].to_numpy()}).filter(~pl.col('same'))
|
| 191 |
+
sagg = sp.group_by('r').agg(pl.col('sim').max().alias('svd_maxsim'), pl.col('sim').filter(pl.col('pos') == 1).max().alias('svd_last'),
|
| 192 |
+
pl.col('sim').top_k(3).mean().alias('svd_top3'))
|
| 193 |
+
R = R.with_columns(pl.Series('svd_cos', svd_cos), pl.Series('svd_ucos', svd_ucos), pl.Series('svd_hnorm', np.linalg.norm(hv, axis=1)),
|
| 194 |
+
pl.Series('svd32_cos', svd32_cos), *[pl.Series(k, v) for k, v in extra_svd.items()])
|
| 195 |
+
R = R.join(sagg, on='r', how='left').sort('r')
|
| 196 |
+
|
| 197 |
+
fill0 = ['i_imp', 'i_clk', 'i_imp1d', 'i_clk1d', 'ip_n', 'ip_users', 'ip_1d', 'ip_3d', 'ip_dec1', 'ip_dec3', 'ip_warm',
|
| 198 |
+
'u_ev', 'u_clk', 'up_n', 'up_uniq', 'up_1d', 'ui_imp', 'ui_clk', 'ui_noclk', 'uip_n', 'co_sum', 'co_max', 'co_mean', 'co_rw',
|
| 199 |
+
'co_cnt', 'co_last3', 'inh_cnt', 'co_nz', 'tr_last', 'trp_last', 'tr_last3', 'trp_last3']
|
| 200 |
+
R = R.with_columns([pl.col(c).fill_null(0) for c in fill0])
|
| 201 |
+
R = R.with_columns(pl.col('i_ctr').fill_null(g), pl.col('i_ctr1d').fill_null(g), pl.col('i_ctr_tb').fill_null(g), pl.col('u_ctr').fill_null(g * 10))
|
| 202 |
+
# within-event relative features
|
| 203 |
+
rel = ['i_ctr', 'ip_dec1', 'ip_n', 'co_sum', 'co_rw', 'co_max', 'svd_cos', 'svd_ucos', 'svd32_cos', 'svd16_cos', 'svd24_cos', 'svd48_cos', 'svd_maxsim', 'svd_top3', 'tr_last3', 'uip_n', 'i_ctr_tb', 'ui_noclk']
|
| 204 |
+
R = R.with_columns([pl.col(c).rank('average').over('eid').alias(c + '_rk') for c in rel] +
|
| 205 |
+
[(pl.col(c) - pl.col(c).mean().over('eid')).alias(c + '_dm') for c in rel] +
|
| 206 |
+
[(pl.col(c) / (pl.col(c).max().over('eid') + 1e-9)).alias(c + '_rmax') for c in ['co_sum', 'svd_cos', 'ip_dec1', 'co_rw']])
|
| 207 |
+
R = R.with_columns(pl.col('co_rw').max().over('eid').alias('ev_co_max'), pl.col('svd_cos').max().over('eid').alias('ev_svd_max'),
|
| 208 |
+
pl.col('svd_cos').mean().over('eid').alias('ev_svd_mean'),
|
| 209 |
+
(pl.col('inh_cnt') > 0).sum().over('eid').alias('ev_n_inh'), pl.col('i_ctr').mean().over('eid').alias('ev_ictr_mean'),
|
| 210 |
+
pl.col('ip_dec1').mean().over('eid').alias('ev_ipdec_mean'))
|
| 211 |
+
log(f'build C={C} rows={len(R)} {time.time()-t0:.0f}s')
|
| 212 |
+
starter = dict(g=g)
|
| 213 |
+
return R
|
| 214 |
+
|
| 215 |
+
DROP = ['r', 'rowid', 'eid', 'user', 'ii', 'h', 'h_ts', 'ts', 'clicked', 'u_last_ev', 'up_last', 'ui_last_imp', 'uip_last', 'fold']
|
parallel/src_snapshot/v2work/prep.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import polars as pl, time, sys
|
| 2 |
+
D='/workspace/huawei/v2work'
|
| 3 |
+
def hexid(e, n=15): # opaque id -> int64 via first 15 hex chars after prefix
|
| 4 |
+
return e.str.slice(4, n).str.to_integer(base=16)
|
| 5 |
+
def prep(name, has_label):
|
| 6 |
+
t=time.time()
|
| 7 |
+
lf=pl.scan_csv(f'{D}/{name}.csv', schema_overrides={'history_item_ids':pl.Utf8,'history_timestamps':pl.Utf8})
|
| 8 |
+
cols=[pl.col('rowid'), hexid(pl.col('event_id')).alias('eid'), hexid(pl.col('item_id')).alias('item')]
|
| 9 |
+
if has_label: cols.append(pl.col('clicked').cast(pl.Int8))
|
| 10 |
+
lf.select(cols).sink_parquet(f'{D}/{name}_rows.parquet')
|
| 11 |
+
print(name,'rows done',time.time()-t,flush=True)
|
| 12 |
+
ev=(lf.select('event_id','user_id','timestamp','time_bucket','day_type','history_item_ids','history_timestamps')
|
| 13 |
+
.unique(subset='event_id',keep='any')
|
| 14 |
+
.select(hexid(pl.col('event_id')).alias('eid'), hexid(pl.col('user_id')).alias('user'),
|
| 15 |
+
pl.col('timestamp').cast(pl.Int64).alias('ts'), pl.col('time_bucket').cast(pl.Int8).alias('tb'),
|
| 16 |
+
pl.col('day_type').cast(pl.Int8).alias('dt'),
|
| 17 |
+
pl.col('history_item_ids').str.json_decode(pl.List(pl.Utf8)).list.eval(pl.element().str.slice(4,15).str.to_integer(base=16)).alias('h_items'),
|
| 18 |
+
pl.col('history_timestamps').str.json_decode(pl.List(pl.Int64)).alias('h_ts')))
|
| 19 |
+
ev.collect(engine='streaming').sort('ts').write_parquet(f'{D}/{name}_events.parquet')
|
| 20 |
+
print(name,'events done',time.time()-t,flush=True)
|
| 21 |
+
prep('evaluation',False)
|
| 22 |
+
prep('train',True)
|
parallel/src_snapshot/v2work/starter_split.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import csv, polars as pl, sys
|
| 2 |
+
csv.field_size_limit(10**9)
|
| 3 |
+
sys.path.insert(0,'/workspace/huawei/v2work')
|
| 4 |
+
D='/workspace/huawei/v2work'; C=1737331200+4*86400
|
| 5 |
+
te=pl.read_parquet(f'{D}/train_events.parquet',columns=['eid','user','ts'])
|
| 6 |
+
v=set(te.filter(pl.col('ts')>=C).sort('ts').group_by('user',maintain_order=True).first()['eid'].to_list())
|
| 7 |
+
with open(f'{D}/train.csv',newline='') as f, open(f'{D}/st_fit.csv','w',newline='') as a, open(f'{D}/st_val.csv','w',newline='') as b, open(f'{D}/st_val_labels.csv','w',newline='') as c:
|
| 8 |
+
r=csv.reader(f); h=next(r); wa=csv.writer(a); wb=csv.writer(b); wc=csv.writer(c)
|
| 9 |
+
wa.writerow(h); wb.writerow(h[:-1]); wc.writerow(['rowid','clicked'])
|
| 10 |
+
for row in r:
|
| 11 |
+
if int(row[4])<C: wa.writerow(row)
|
| 12 |
+
elif int(row[1][4:19],16) in v: wb.writerow(row[:-1]); wc.writerow([row[0],row[-1]])
|
parallel/src_snapshot/v2work/starter_val.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import polars as pl, numpy as np, math, sys
|
| 2 |
+
sys.path.insert(0,'/workspace/huawei/v2work')
|
| 3 |
+
from feats import load, first_events
|
| 4 |
+
from sklearn.metrics import roc_auc_score
|
| 5 |
+
D='/workspace/huawei/v2work'
|
| 6 |
+
TE,TR,EE,ER,NI=load(D)
|
| 7 |
+
C=1737763200; W=2*86400
|
| 8 |
+
T_ev=first_events(TE,C,W); T_rows=TR.join(T_ev.select('eid'),on='eid')
|
| 9 |
+
P_ev=TE.filter(pl.col('ts')<C); P=TR.join(P_ev.select('eid','user'),on='eid')
|
| 10 |
+
g=P['clicked'].mean(); K=50
|
| 11 |
+
ic=P.group_by('ii').agg(((pl.col('clicked').sum()+K*g)/(pl.len()+K)).alias('ictr'))
|
| 12 |
+
uc=P.group_by('user').agg(((pl.col('clicked').sum()+K*g)/(pl.len()+K)).alias('uctr'))
|
| 13 |
+
# user_interacted: union of history items of train rows' events + clicked items
|
| 14 |
+
inter=pl.concat([P_ev.select('user','h').explode('h').drop_nulls().rename({'h':'ii'}).with_columns(pl.col('ii').cast(pl.Int32)),
|
| 15 |
+
P.filter(pl.col('clicked')==1).select('user',pl.col('ii').cast(pl.Int32))]).unique()
|
| 16 |
+
pop=inter.group_by('ii').agg(pl.len().alias('pop'))
|
| 17 |
+
pairs=inter.join(inter,on='user',suffix='_b').filter(pl.col('ii')!=pl.col('ii_b')).group_by('ii','ii_b').agg(pl.len().alias('c'))
|
| 18 |
+
pairs=pairs.join(pop,on='ii').join(pop.rename({'ii':'ii_b','pop':'pop_b'}),on='ii_b').with_columns((pl.col('c')/(pl.col('pop')*pl.col('pop_b')).sqrt()).alias('cs')).select('ii','ii_b','cs')
|
| 19 |
+
R=T_rows.join(T_ev.select('eid','user','h'),on='eid').with_row_index('r')
|
| 20 |
+
PR=R.select('r',pl.col('ii').cast(pl.Int32),'h').explode('h').drop_nulls('h').rename({'h':'ii_b'}).with_columns(pl.col('ii_b').cast(pl.Int32))
|
| 21 |
+
cf=PR.join(pairs,on=['ii','ii_b'],how='left').group_by('r').agg(pl.col('cs').fill_null(0).sum().alias('cf'))
|
| 22 |
+
R=R.join(ic,on='ii',how='left').join(uc,on='user',how='left').join(cf,on='r',how='left').with_columns(pl.col('ictr').fill_null(g),pl.col('uctr').fill_null(g),pl.col('cf').fill_null(0))
|
| 23 |
+
lg=lambda p: np.log(np.clip(p,1e-6,1-1e-6)/(1-np.clip(p,1e-6,1-1e-6)))
|
| 24 |
+
z=lg(R['ictr'].to_numpy())+lg(R['uctr'].to_numpy())-lg(g)+25*R['cf'].to_numpy()
|
| 25 |
+
y=R['clicked'].to_numpy()
|
| 26 |
+
print('val events',len(T_ev),'rows',len(R),'ctr',y.mean())
|
| 27 |
+
print('starter AUC',roc_auc_score(y,z))
|
| 28 |
+
print('item ctr only',roc_auc_score(y,R['ictr'].to_numpy()),'user ctr only',roc_auc_score(y,R['uctr'].to_numpy()),'cf only',roc_auc_score(y,R['cf'].to_numpy()))
|
| 29 |
+
print('no cf',roc_auc_score(y,lg(R['ictr'].to_numpy())+lg(R['uctr'].to_numpy())))
|
parallel/src_snapshot/v2work/train_val.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import polars as pl, numpy as np, lightgbm as lgb, sys
|
| 2 |
+
sys.path.insert(0,'/workspace/huawei/v2work')
|
| 3 |
+
from feats import DROP
|
| 4 |
+
from sklearn.metrics import roc_auc_score
|
| 5 |
+
D='/workspace/huawei/v2work/feat'
|
| 6 |
+
tr_names=sys.argv[1].split(',') if len(sys.argv)>1 else ['f1','f2','f3','f4']
|
| 7 |
+
obj=sys.argv[2] if len(sys.argv)>2 else 'binary'
|
| 8 |
+
tr=pl.concat([pl.read_parquet(f'{D}/{n}.parquet').with_columns(pl.lit(i).alias('fold')) for i,n in enumerate(tr_names)],how='diagonal_relaxed').sort('fold','eid','r')
|
| 9 |
+
import os
|
| 10 |
+
VAL=os.environ.get('VAL','val')
|
| 11 |
+
va=pl.read_parquet(f'{D}/{VAL}.parquet').sort('eid','r')
|
| 12 |
+
feats=[c for c in tr.columns if c not in DROP]
|
| 13 |
+
excl=[x for x in (sys.argv[3].split(',') if len(sys.argv)>3 and sys.argv[3] else [])]
|
| 14 |
+
feats=[f for f in feats if not any(f.startswith(e) for e in excl)]
|
| 15 |
+
Xtr=tr.select(feats).to_numpy().astype(np.float32); ytr=tr['clicked'].to_numpy()
|
| 16 |
+
Xva=va.select(feats).to_numpy().astype(np.float32); yva=va['clicked'].to_numpy()
|
| 17 |
+
LR=float(os.environ.get('LR','0.02'));NL=int(os.environ.get('NL','31'))
|
| 18 |
+
p=dict(objective=obj,learning_rate=LR,num_leaves=NL,min_data_in_leaf=500,feature_fraction=0.5,bagging_fraction=0.8,bagging_freq=1,lambda_l2=10,verbose=-1,num_threads=8,metric='auc')
|
| 19 |
+
if obj=='lambdarank':
|
| 20 |
+
p.update(metric='ndcg',eval_at=[10])
|
| 21 |
+
gtr=tr.group_by(['fold','eid'],maintain_order=True).len()['len'].to_numpy(); gva=va.group_by('eid',maintain_order=True).len()['len'].to_numpy()
|
| 22 |
+
dtr=lgb.Dataset(Xtr,ytr,group=gtr,feature_name=feats); dva=lgb.Dataset(Xva,yva,group=gva,reference=dtr)
|
| 23 |
+
else:
|
| 24 |
+
dtr=lgb.Dataset(Xtr,ytr,feature_name=feats); dva=lgb.Dataset(Xva,yva,reference=dtr)
|
| 25 |
+
aucs=[]
|
| 26 |
+
def cb(env):
|
| 27 |
+
if env.iteration%int(os.environ.get('EV','50'))==int(os.environ.get('EV','50'))-1:
|
| 28 |
+
pr=env.model.predict(Xva,num_iteration=env.iteration+1); a=roc_auc_score(yva,pr); aucs.append((env.iteration+1,a)); print(env.iteration+1,round(a,5),flush=True)
|
| 29 |
+
m=lgb.train(p,dtr,num_boost_round=int(sys.argv[4]) if len(sys.argv)>4 else 1500,callbacks=[cb])
|
| 30 |
+
best=max(aucs,key=lambda x:x[1]); print('BEST',obj,tr_names,excl,best)
|
| 31 |
+
imp=sorted(zip(feats,m.feature_importance('gain')),key=lambda x:-x[1])
|
| 32 |
+
tot=sum(v for _,v in imp)
|
| 33 |
+
for f,v in imp[:30]: print(f'{f:22s} {v/tot:.4f}')
|
| 34 |
+
np.save(f'/workspace/huawei/v2work/pred_{VAL}_{obj}.npy',m.predict(Xva,num_iteration=best[0]))
|
| 35 |
+
pb=m.predict(Xva,num_iteration=best[0])
|
| 36 |
+
def seg(name,mask):
|
| 37 |
+
if mask.sum()>0 and 0<yva[mask].sum()<mask.sum(): print(f' seg {name:14s} rows={mask.sum():7d} auc={roc_auc_score(yva[mask],pb[mask]):.4f}')
|
| 38 |
+
u_ev=va['u_ev'].to_numpy(); hl=va['h_len'].to_numpy(); dt=va['dt'].to_numpy()
|
| 39 |
+
seg('all',np.ones(len(yva),bool)); seg('warm',u_ev>0); seg('hist_only',(u_ev==0)&(hl>0)); seg('empty_hist',hl==0); seg('weekday',dt==0); seg('weekend',dt==1)
|
parallel/src_snapshot/v2work/tt.py
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Two-tower click model trained on events before cutoff: user = position-weighted sum of
|
| 2 |
+
history item embeddings; score = <user, item_out> + item bias. Trained with BCE per candidate
|
| 3 |
+
plus an in-event softmax term (with a learned 'no click' option)."""
|
| 4 |
+
import torch, numpy as np, polars as pl, time
|
| 5 |
+
torch.set_num_threads(4)
|
| 6 |
+
H=20
|
| 7 |
+
def pack(E):
|
| 8 |
+
"""events -> padded history matrix [n,H] (newest last, right-aligned), mask, plus delta-time buckets"""
|
| 9 |
+
n=len(E); M=np.full((n,H),-1,np.int64); DT=np.zeros((n,H),np.float32)
|
| 10 |
+
hl=E['h'].to_list(); htl=E['h_ts'].to_list(); ts=E['ts'].to_numpy()
|
| 11 |
+
for i,(h,t) in enumerate(zip(hl,htl)):
|
| 12 |
+
if h:
|
| 13 |
+
h=h[-H:]; t=t[-H:]; M[i,H-len(h):]=h; DT[i,H-len(h):]=np.log1p((ts[i]-np.array(t))/3600.)
|
| 14 |
+
return M,DT
|
| 15 |
+
class TT(torch.nn.Module):
|
| 16 |
+
def __init__(s,NI,d=64):
|
| 17 |
+
super().__init__()
|
| 18 |
+
s.ein=torch.nn.Embedding(NI+1,d,padding_idx=NI); s.eout=torch.nn.Embedding(NI,d); s.b=torch.nn.Embedding(NI,1)
|
| 19 |
+
s.pw=torch.nn.Parameter(torch.zeros(H)); s.tw=torch.nn.Parameter(torch.zeros(1)); s.b0=torch.nn.Parameter(torch.tensor(-2.7))
|
| 20 |
+
s.null=torch.nn.Linear(d,1)
|
| 21 |
+
for e in (s.ein,s.eout): torch.nn.init.normal_(e.weight,0,0.05)
|
| 22 |
+
torch.nn.init.zeros_(s.b.weight); s.NI=NI
|
| 23 |
+
def user(s,M,DT):
|
| 24 |
+
mask=(M>=0); Mi=torch.where(mask,M,torch.full_like(M,s.NI))
|
| 25 |
+
w=torch.exp(s.pw[None,:]-torch.nn.functional.softplus(s.tw)*DT)*mask
|
| 26 |
+
u=(s.ein(Mi)*w[...,None]).sum(1)/(w.sum(1,keepdim=True)+1e-6)
|
| 27 |
+
return u
|
| 28 |
+
def forward(s,M,DT,C):
|
| 29 |
+
u=s.user(M,DT); return (u[:,None,:]*s.eout(C)).sum(-1)+s.b(C)[...,0]+s.b0, s.null(u)[:,0]
|
| 30 |
+
def make_xy(E,R,with_y=True):
|
| 31 |
+
R=R.join(E.select('eid').with_row_index('ev'),on='eid').sort('ev')
|
| 32 |
+
g=R.group_by('ev',maintain_order=True).agg(pl.col('ii'),*( [pl.col('clicked')] if with_y else []))
|
| 33 |
+
C=np.array(g['ii'].to_list(),np.int64); Y=np.array(g['clicked'].to_list(),np.float32) if with_y else None
|
| 34 |
+
return C,Y,R
|
| 35 |
+
def train(P_ev,P_rows,NI,epochs=3,d=64,bs=512,lr=3e-3,wd=1e-6,seed=0,log=print):
|
| 36 |
+
torch.manual_seed(seed); np.random.seed(seed)
|
| 37 |
+
M,DT=pack(P_ev); C,Y,_=make_xy(P_ev,P_rows)
|
| 38 |
+
M,DT,C,Y=map(torch.from_numpy,(M,DT,C,Y))
|
| 39 |
+
m=TT(NI,d); opt=torch.optim.Adam(m.parameters(),lr=lr)
|
| 40 |
+
n=len(M); t=time.time()
|
| 41 |
+
for ep in range(epochs):
|
| 42 |
+
perm=torch.randperm(n); tot=0
|
| 43 |
+
for k in range(0,n,bs):
|
| 44 |
+
idx=perm[k:k+bs]; s,nl=m(M[idx],DT[idx],C[idx]); y=Y[idx]
|
| 45 |
+
bce=torch.nn.functional.binary_cross_entropy_with_logits(s,y)
|
| 46 |
+
logits=torch.cat([s,nl[:,None]],1); tgt=torch.where(y.sum(1)>0,y.argmax(1),torch.full((len(y),),10))
|
| 47 |
+
ce=torch.nn.functional.cross_entropy(logits,tgt)
|
| 48 |
+
reg=wd*(m.ein.weight[M[idx].clamp(min=0)].pow(2).sum()+m.eout.weight[C[idx]].pow(2).sum())/len(idx)
|
| 49 |
+
loss=bce+0.1*ce+reg; opt.zero_grad(); loss.backward(); opt.step(); tot+=loss.item()*len(idx)
|
| 50 |
+
log(f'tt epoch {ep} loss {tot/n:.4f} {time.time()-t:.0f}s')
|
| 51 |
+
return m
|
| 52 |
+
@torch.no_grad()
|
| 53 |
+
def score(m,T_ev,T_rows):
|
| 54 |
+
"""returns T_rows (eid,ii) joined with tt_score, tt_null, tt_unorm"""
|
| 55 |
+
m.eval(); M,DT=pack(T_ev); C,_,R=make_xy(T_ev,T_rows,with_y=False)
|
| 56 |
+
s,nl=m(torch.from_numpy(M),torch.from_numpy(DT),torch.from_numpy(C)); u=m.user(torch.from_numpy(M),torch.from_numpy(DT))
|
| 57 |
+
out=pl.DataFrame({'eid':np.repeat(T_ev['eid'].to_numpy(),10),'ii':C.ravel().astype(np.int32),'tt_score':s.numpy().ravel(),
|
| 58 |
+
'tt_null':np.repeat(nl.numpy(),10),'tt_unorm':np.repeat(u.norm(dim=1).numpy(),10)})
|
| 59 |
+
return out
|
parallel/src_snapshot/v2work/tt_exp.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import polars as pl, numpy as np, sys, time
|
| 2 |
+
sys.path.insert(0,'/workspace/huawei/v2work')
|
| 3 |
+
from feats import load, first_events
|
| 4 |
+
import tt
|
| 5 |
+
from sklearn.metrics import roc_auc_score
|
| 6 |
+
TE,TR,EE,ER,NI=load('/workspace/huawei/v2work')
|
| 7 |
+
DAY=86400; C=1737331200+4*DAY
|
| 8 |
+
T_ev=first_events(TE,C,3*DAY); T_rows=TR.join(T_ev.select('eid'),on='eid')
|
| 9 |
+
P_ev=TE.filter(pl.col('ts')<C); P_rows=TR.join(P_ev.select('eid'),on='eid')
|
| 10 |
+
ep=int(sys.argv[1]) if len(sys.argv)>1 else 3; d=int(sys.argv[2]) if len(sys.argv)>2 else 64
|
| 11 |
+
m=tt.train(P_ev,P_rows,NI,epochs=ep,d=d,wd=float(sys.argv[3]) if len(sys.argv)>3 else 1e-6)
|
| 12 |
+
out=tt.score(m,T_ev,T_rows).join(T_rows.select('eid',pl.col('ii').cast(pl.Int32),'clicked'),on=['eid','ii'])
|
| 13 |
+
out=out.with_columns((pl.col('tt_score')-pl.col('tt_score').mean().over('eid')).alias('dm'))
|
| 14 |
+
y=out['clicked'].to_numpy()
|
| 15 |
+
print('TT',ep,d,'auc',roc_auc_score(y,out['tt_score']),'dm',roc_auc_score(y,out['dm']))
|
parallel/src_snapshot/v2work/v3feat_ne.py
ADDED
|
@@ -0,0 +1,290 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Feature builder (as-of-cutoff, leakage-safe) for TechArena CTR.
|
| 2 |
+
|
| 3 |
+
build(C, T_ev, T_rows, TE, TR) returns one feature row per target candidate.
|
| 4 |
+
Every statistic uses ONLY:
|
| 5 |
+
* train events/rows with ts < C (fit window: impressions + labels), and
|
| 6 |
+
* click histories of events with ts < C, plus the target event's OWN history (strictly before its ts).
|
| 7 |
+
Since target events are each user's first event at/after C, their histories contain only clicks < C,
|
| 8 |
+
so nothing observed after a target row's own event time is ever joined.
|
| 9 |
+
|
| 10 |
+
Feature groups (column prefixes, used for ablation):
|
| 11 |
+
hist_ candidate vs the user's click log / own history / past impressions of that user-item pair
|
| 12 |
+
it_ item popularity & CTR (full fit window, last 24h, last 6h, trend)
|
| 13 |
+
co_ item-item co-click (user co-occurrence cosine) and sequential transitions vs user's last 5/20 clicks
|
| 14 |
+
us_ user activity / CTR / recency / cold-start flags
|
| 15 |
+
ev_ within-event rank / z-score / event max & mean of the main features
|
| 16 |
+
tm_ time_bucket, day_type, hour, staleness since cutoff
|
| 17 |
+
sv_ truncated SVD (tf-idf, row-normalised user x item click matrix as of C): user/history/item cosines
|
| 18 |
+
mf_ logistic matrix factorisation score fitted on fit-window impressions
|
| 19 |
+
"""
|
| 20 |
+
import time
|
| 21 |
+
import numpy as np
|
| 22 |
+
import polars as pl
|
| 23 |
+
from scipy import sparse
|
| 24 |
+
from sklearn.utils.extmath import randomized_svd
|
| 25 |
+
|
| 26 |
+
J20 = 1737331200
|
| 27 |
+
DAY = 86400
|
| 28 |
+
G_PRIOR = 0.0629
|
| 29 |
+
K_ITEM = 20 # pseudo-impressions for item CTR shrinkage
|
| 30 |
+
K_USER = 5 # pseudo-events for user CTR shrinkage
|
| 31 |
+
TOPK = 200 # co-click neighbours kept per item
|
| 32 |
+
ID_COLS = ['rowid', 'eid', 'u', 'ii', 'ts', 'clicked', 'ord']
|
| 33 |
+
_t0 = time.time()
|
| 34 |
+
|
| 35 |
+
def log(*a):
|
| 36 |
+
print(time.strftime('%H:%M:%S'), *a, flush=True)
|
| 37 |
+
|
| 38 |
+
def load(D):
|
| 39 |
+
TE = pl.read_parquet(f'{D}/ev_train.parquet')
|
| 40 |
+
TR = pl.read_parquet(f'{D}/rows_train.parquet')
|
| 41 |
+
EE = pl.read_parquet(f'{D}/ev_eval.parquet')
|
| 42 |
+
ER = pl.read_parquet(f'{D}/rows_eval.parquet')
|
| 43 |
+
NI = pl.read_parquet(f'{D}/items.parquet').height
|
| 44 |
+
NU = pl.read_parquet(f'{D}/users.parquet').height
|
| 45 |
+
return TE, TR, EE, ER, NI, NU
|
| 46 |
+
|
| 47 |
+
def _hist_long(E):
|
| 48 |
+
return (E.select('u', 'h', 'h_ts').explode(['h', 'h_ts']).drop_nulls('h')
|
| 49 |
+
.select('u', pl.col('h').cast(pl.Int32).alias('ii'), pl.col('h_ts').cast(pl.Int64).alias('ts')))
|
| 50 |
+
|
| 51 |
+
def click_log(C, P_ev, P_rows, T_ev):
|
| 52 |
+
"""Exact (user,item,ts) click log as of C: fit-window clicks + warm-up clicks seen in pre-C / own histories."""
|
| 53 |
+
win = P_rows.filter(pl.col('clicked') == 1).select('u', pl.col('ii').cast(pl.Int32), 'ts')
|
| 54 |
+
hist = pl.concat([_hist_long(P_ev), _hist_long(T_ev)]).filter(pl.col('ts') < C)
|
| 55 |
+
CL = pl.concat([win, hist]).unique().sort('u', 'ts', 'ii')
|
| 56 |
+
return CL
|
| 57 |
+
|
| 58 |
+
def cooc_tables(CL, NI):
|
| 59 |
+
"""User co-occurrence cosine (top-K per item) + lag-weighted transition probabilities prev->next."""
|
| 60 |
+
us = CL.select('u', 'ii').unique()
|
| 61 |
+
pop = us.group_by('ii').agg(pl.len().alias('pop'))
|
| 62 |
+
pairs = (us.join(us, on='u', suffix='_b').filter(pl.col('ii') != pl.col('ii_b'))
|
| 63 |
+
.group_by('ii', 'ii_b').agg(pl.len().cast(pl.Int32).alias('co')))
|
| 64 |
+
co = (pairs.join(pop, on='ii').join(pop.rename({'ii': 'ii_b', 'pop': 'pop_b'}), on='ii_b')
|
| 65 |
+
.with_columns((pl.col('co') / (pl.col('pop') * pl.col('pop_b')).sqrt()).cast(pl.Float32).alias('cos'))
|
| 66 |
+
.select(pl.col('ii_b').alias('j'), 'ii', 'cos') # j = history item, ii = candidate
|
| 67 |
+
.sort('cos', descending=True).group_by('j').head(TOPK))
|
| 68 |
+
seq = CL.select('u', 'ii') # already sorted by u, ts
|
| 69 |
+
parts = []
|
| 70 |
+
for lag in (1, 2, 3):
|
| 71 |
+
parts.append(seq.with_columns(pl.col('ii').shift(lag).over('u').alias('j')).drop_nulls('j')
|
| 72 |
+
.select(pl.col('j').cast(pl.Int32), 'ii', pl.lit(1.0 / lag).alias('w')))
|
| 73 |
+
trn = pl.concat(parts).filter(pl.col('j') != pl.col('ii')).group_by('j', 'ii').agg(pl.col('w').sum())
|
| 74 |
+
tot = trn.group_by('j').agg(pl.col('w').sum().alias('tot'))
|
| 75 |
+
trn = (trn.join(tot, on='j').with_columns((pl.col('w') / (pl.col('tot') + 5.0)).cast(pl.Float32).alias('trp'))
|
| 76 |
+
.select('j', 'ii', 'trp').sort('trp', descending=True).group_by('j').head(TOPK))
|
| 77 |
+
return co, trn
|
| 78 |
+
|
| 79 |
+
def fit_mf(P_rows, NU, NI, dim=24, epochs=6, lr=0.05, l2=1e-4, seed=0, batch=200_000):
|
| 80 |
+
"""Logistic MF logit = b + bu + bi + <pu, qi> fitted with Adagrad on fit-window impressions (numpy)."""
|
| 81 |
+
rng = np.random.default_rng(seed)
|
| 82 |
+
u = P_rows['u'].to_numpy().astype(np.int64); i = P_rows['ii'].to_numpy().astype(np.int64)
|
| 83 |
+
y = P_rows['clicked'].to_numpy().astype(np.float32)
|
| 84 |
+
P = (rng.standard_normal((NU, dim)) * 0.01).astype(np.float32); Q = (rng.standard_normal((NI, dim)) * 0.01).astype(np.float32)
|
| 85 |
+
bu = np.zeros(NU, np.float32); bi = np.zeros(NI, np.float32)
|
| 86 |
+
b = np.float32(np.log(y.mean() / (1 - y.mean())))
|
| 87 |
+
gP = np.full_like(P, 1e-6); gQ = np.full_like(Q, 1e-6); gbu = np.full_like(bu, 1e-6); gbi = np.full_like(bi, 1e-6)
|
| 88 |
+
n = len(y)
|
| 89 |
+
for ep in range(epochs):
|
| 90 |
+
perm = rng.permutation(n)
|
| 91 |
+
for s in range(0, n, batch):
|
| 92 |
+
idx = perm[s:s + batch]; ub, ib, yb = u[idx], i[idx], y[idx]
|
| 93 |
+
pu, qi = P[ub], Q[ib]
|
| 94 |
+
z = b + bu[ub] + bi[ib] + (pu * qi).sum(1)
|
| 95 |
+
g = (1 / (1 + np.exp(-z)) - yb).astype(np.float32) # dL/dz
|
| 96 |
+
m = len(idx)
|
| 97 |
+
Su = sparse.csr_matrix((np.ones(m, np.float32), (ub, np.arange(m))), shape=(NU, m))
|
| 98 |
+
Si = sparse.csr_matrix((np.ones(m, np.float32), (ib, np.arange(m))), shape=(NI, m))
|
| 99 |
+
dP = Su @ (g[:, None] * qi) + l2 * P * (np.asarray(Su.sum(1)).ravel() > 0)[:, None]
|
| 100 |
+
dQ = Si @ (g[:, None] * pu) + l2 * Q * (np.asarray(Si.sum(1)).ravel() > 0)[:, None]
|
| 101 |
+
dbu = Su @ g; dbi = Si @ g
|
| 102 |
+
gP += dP * dP; gQ += dQ * dQ; gbu += dbu * dbu; gbi += dbi * dbi
|
| 103 |
+
P -= lr * dP / np.sqrt(gP); Q -= lr * dQ / np.sqrt(gQ)
|
| 104 |
+
bu -= lr * dbu / np.sqrt(gbu); bi -= lr * dbi / np.sqrt(gbi)
|
| 105 |
+
return dict(b=b, bu=bu, bi=bi, P=P, Q=Q)
|
| 106 |
+
|
| 107 |
+
def build(C, T_ev, T_rows, TE, TR, NI, NU, with_mf=True, sv_mode='std'):
|
| 108 |
+
t0 = time.time()
|
| 109 |
+
P_ev = TE.filter(pl.col('ts') < C)
|
| 110 |
+
P_rows = TR.join(P_ev.select('eid', 'u', 'ts'), on='eid', how='inner')
|
| 111 |
+
g = float(P_rows['clicked'].mean())
|
| 112 |
+
ndays = (C - J20) / DAY
|
| 113 |
+
CL = click_log(C, P_ev, P_rows, T_ev) # per-user stats: train data + each target row's OWN history
|
| 114 |
+
CLg = click_log(C, P_ev, P_rows, T_ev.head(0)) # item-level / global stats: train data only (no target-event histories)
|
| 115 |
+
|
| 116 |
+
# ---------------- item ----------------
|
| 117 |
+
def ictr(c, n):
|
| 118 |
+
return (pl.col(c) + K_ITEM * g) / (pl.col(n) + K_ITEM)
|
| 119 |
+
it = P_rows.group_by('ii').agg(
|
| 120 |
+
pl.len().alias('it_imp'), pl.col('clicked').sum().cast(pl.Int32).alias('it_clk'),
|
| 121 |
+
(pl.col('ts') >= C - DAY).sum().alias('it_imp24'), pl.col('clicked').filter(pl.col('ts') >= C - DAY).sum().cast(pl.Int32).alias('it_clk24'),
|
| 122 |
+
(pl.col('ts') >= C - 6 * 3600).sum().alias('it_imp6'), pl.col('clicked').filter(pl.col('ts') >= C - 6 * 3600).sum().cast(pl.Int32).alias('it_clk6'))
|
| 123 |
+
it = it.with_columns(ictr('it_clk', 'it_imp').alias('it_ctr'), ictr('it_clk24', 'it_imp24').alias('it_ctr24'),
|
| 124 |
+
ictr('it_clk6', 'it_imp6').alias('it_ctr6'))
|
| 125 |
+
it = it.with_columns((pl.col('it_ctr24') / pl.col('it_ctr')).alias('it_trend24'), (pl.col('it_ctr6') / pl.col('it_ctr')).alias('it_trend6'),
|
| 126 |
+
(pl.col('it_imp24') / (pl.col('it_imp') / ndays)).alias('it_imptrend24'),
|
| 127 |
+
(pl.col('it_imp') / ndays).alias('it_imp_rate'))
|
| 128 |
+
ip = CLg.group_by('ii').agg(pl.len().alias('it_nclk_all'), pl.col('u').n_unique().alias('it_nusers'),
|
| 129 |
+
(pl.col('ts') < J20).sum().alias('it_nclk_warm'),
|
| 130 |
+
((pl.col('ts') - C) / (2 * DAY)).exp().sum().cast(pl.Float32).alias('it_clk_dec2d'))
|
| 131 |
+
|
| 132 |
+
# ---------------- user ----------------
|
| 133 |
+
ue = P_rows.group_by('u').agg(pl.col('eid').n_unique().alias('us_nev'), pl.col('clicked').sum().cast(pl.Int32).alias('us_nclk_win'),
|
| 134 |
+
pl.col('ts').max().alias('_u_last_ev'))
|
| 135 |
+
ue = ue.with_columns(((pl.col('us_nclk_win') + K_USER * 0.629) / (pl.col('us_nev') + K_USER)).alias('us_ctr'))
|
| 136 |
+
uc = CL.group_by('u').agg(pl.len().alias('us_nclk_all'), (pl.col('ts') < J20).sum().alias('us_nclk_warm'),
|
| 137 |
+
pl.col('ii').n_unique().alias('us_nuniq'), pl.col('ts').max().alias('_u_last_clk'),
|
| 138 |
+
(pl.col('ts') >= C - DAY).sum().alias('us_nclk24'))
|
| 139 |
+
uc = uc.with_columns((1 - pl.col('us_nuniq') / pl.col('us_nclk_all')).alias('us_repeat_rate'))
|
| 140 |
+
|
| 141 |
+
# ---------------- user x item ----------------
|
| 142 |
+
uic = CL.group_by('u', 'ii').agg(pl.len().alias('hist_n_all'), (pl.col('ts') < J20).sum().alias('hist_n_warm'),
|
| 143 |
+
(pl.col('ts') >= J20).sum().alias('hist_n_win'), pl.col('ts').max().alias('_ui_last_clk'))
|
| 144 |
+
uii = P_rows.group_by('u', 'ii').agg(pl.len().alias('hist_imp'), pl.col('clicked').sum().cast(pl.Int32).alias('_ui_clk'),
|
| 145 |
+
pl.col('ts').max().alias('_ui_last_imp'))
|
| 146 |
+
uii = uii.with_columns((pl.col('hist_imp') - pl.col('_ui_clk')).alias('hist_noclk'))
|
| 147 |
+
|
| 148 |
+
# ---------------- assemble ----------------
|
| 149 |
+
R = (T_rows.select([c for c in ('rowid', 'eid', 'ii', 'clicked', 'ord') if c in T_rows.columns])
|
| 150 |
+
.join(T_ev.select('eid', 'u', 'ts', 'tb', 'dt', 'h', 'h_ts'), on='eid', how='left').with_row_index('r'))
|
| 151 |
+
R = (R.join(it, on='ii', how='left').join(ip, on='ii', how='left').join(ue, on='u', how='left').join(uc, on='u', how='left')
|
| 152 |
+
.join(uic, on=['u', 'ii'], how='left').join(uii, on=['u', 'ii'], how='left'))
|
| 153 |
+
R = R.with_columns(
|
| 154 |
+
pl.col('h').list.len().alias('us_hlen'),
|
| 155 |
+
(pl.col('h').list.len() == 0).cast(pl.Int8).alias('us_nohist'),
|
| 156 |
+
pl.col('us_nev').is_null().cast(pl.Int8).alias('us_cold'),
|
| 157 |
+
(pl.col('ts') - pl.col('_u_last_ev')).alias('us_since_ev'),
|
| 158 |
+
(pl.col('ts') - pl.col('_u_last_clk')).alias('us_since_clk'),
|
| 159 |
+
(pl.col('ts') - pl.col('_ui_last_clk')).alias('hist_since_clk'),
|
| 160 |
+
(pl.col('ts') - pl.col('_ui_last_imp')).alias('hist_since_imp'),
|
| 161 |
+
(pl.col('ts') - C).alias('tm_stale'), ((pl.col('ts') // 3600) % 24).cast(pl.Int8).alias('tm_hour'),
|
| 162 |
+
pl.col('tb').alias('tm_tb'), pl.col('dt').alias('tm_dt'))
|
| 163 |
+
# position (from end, 1 = most recent) of candidate in own history
|
| 164 |
+
hx = (R.select('r', 'ii', 'h').with_columns(pl.int_ranges(pl.col('h').list.len(), 0, -1).alias('pos'))
|
| 165 |
+
.explode(['h', 'pos']).drop_nulls('h').filter(pl.col('h') == pl.col('ii'))
|
| 166 |
+
.group_by('r').agg(pl.col('pos').min().alias('hist_pos'), pl.len().alias('hist_n_own')))
|
| 167 |
+
R = R.join(hx, on='r', how='left')
|
| 168 |
+
R = R.with_columns(pl.col('hist_since_clk').fill_null(30 * DAY), pl.col('hist_since_imp').fill_null(30 * DAY),
|
| 169 |
+
pl.col('hist_pos').fill_null(99), pl.col('hist_n_own').fill_null(0), pl.col('us_since_ev').fill_null(30 * DAY), pl.col('us_since_clk').fill_null(30 * DAY))
|
| 170 |
+
R = R.with_columns((pl.col('hist_n_all').fill_null(0) > 0).cast(pl.Int8).alias('hist_in'))
|
| 171 |
+
|
| 172 |
+
# ---------------- co-click ----------------
|
| 173 |
+
co, trn = cooc_tables(CLg, NI)
|
| 174 |
+
last20 = CL.group_by('u', maintain_order=True).agg(pl.col('ii').tail(20).alias('l20'))
|
| 175 |
+
T_u = R.select('r', 'u', 'ii').join(last20, on='u', how='left')
|
| 176 |
+
X = (T_u.with_columns(pl.int_ranges(pl.col('l20').list.len(), 0, -1).alias('pos')).explode(['l20', 'pos'])
|
| 177 |
+
.drop_nulls('l20').rename({'l20': 'j'}).with_columns(pl.col('j').cast(pl.Int32))
|
| 178 |
+
.join(co, on=['j', 'ii'], how='left').join(trn, on=['j', 'ii'], how='left')
|
| 179 |
+
.with_columns(pl.col('cos').fill_null(0), pl.col('trp').fill_null(0)))
|
| 180 |
+
l5 = pl.col('pos') <= 5
|
| 181 |
+
agg = X.group_by('r').agg(
|
| 182 |
+
pl.col('cos').max().alias('co_cos_max20'), pl.col('cos').mean().alias('co_cos_mean20'),
|
| 183 |
+
pl.col('cos').filter(l5).max().alias('co_cos_max5'), pl.col('cos').filter(l5).mean().alias('co_cos_mean5'),
|
| 184 |
+
pl.col('cos').filter(pl.col('pos') == 1).first().alias('co_cos_last'),
|
| 185 |
+
pl.col('trp').max().alias('co_tr_max20'), pl.col('trp').mean().alias('co_tr_mean20'),
|
| 186 |
+
pl.col('trp').filter(l5).max().alias('co_tr_max5'), pl.col('trp').filter(l5).mean().alias('co_tr_mean5'),
|
| 187 |
+
pl.col('trp').filter(pl.col('pos') == 1).first().alias('co_tr_last'),
|
| 188 |
+
(pl.col('cos') / pl.col('pos').cast(pl.Float32).sqrt()).sum().alias('co_cos_rw'), pl.col('cos').sum().alias('co_cos_sum20'))
|
| 189 |
+
R = R.join(agg, on='r', how='left').sort('r')
|
| 190 |
+
|
| 191 |
+
# ---------------- SVD embeddings ----------------
|
| 192 |
+
# sv_mode (leakage checks only): 'std' = click log as of C; 'notgt' = drop target events' own histories;
|
| 193 |
+
# 'lag1d' = only clicks/impressions with ts < C - 1 day
|
| 194 |
+
if sv_mode == 'std':
|
| 195 |
+
CLsv, NEGsrc = CLg, P_rows
|
| 196 |
+
elif sv_mode == 'notgt':
|
| 197 |
+
CLsv = pl.concat([P_rows.filter(pl.col('clicked') == 1).select('u', pl.col('ii').cast(pl.Int32), 'ts'),
|
| 198 |
+
_hist_long(P_ev).filter(pl.col('ts') < C)]).unique()
|
| 199 |
+
NEGsrc = P_rows
|
| 200 |
+
elif sv_mode == 'lag1d':
|
| 201 |
+
CLsv, NEGsrc = CL.filter(pl.col('ts') < C - DAY), P_rows.filter(pl.col('ts') < C - DAY)
|
| 202 |
+
else:
|
| 203 |
+
raise ValueError(sv_mode)
|
| 204 |
+
assert (CLsv['ts'].max() or 0) < C and (NEGsrc['ts'].max() or 0) < C
|
| 205 |
+
cnt = CLsv.group_by('u', 'ii').agg(pl.len().alias('n'))
|
| 206 |
+
Xc = sparse.csr_matrix((np.ones(cnt.height, np.float32), (cnt['u'].to_numpy(), cnt['ii'].to_numpy())), shape=(NU, NI))
|
| 207 |
+
df_ = np.asarray(Xc.sum(0)).ravel(); idf = np.log((NU + 1) / (df_ + 1)).astype(np.float32)
|
| 208 |
+
Xc = Xc @ sparse.diags(idf); rn = np.sqrt(np.asarray(Xc.multiply(Xc).sum(1)).ravel()) + 1e-6; Xc = sparse.diags(1 / rn) @ Xc
|
| 209 |
+
# user-side matrix incl. each target user's own history (fold-in onto item factors learned from train-only clicks)
|
| 210 |
+
cnt = CL.filter(pl.col('ts') < C).group_by('u', 'ii').agg(pl.len().alias('n')) if sv_mode == 'std' else cnt
|
| 211 |
+
Xu = sparse.csr_matrix((np.ones(cnt.height, np.float32), (cnt['u'].to_numpy(), cnt['ii'].to_numpy())), shape=(NU, NI)) @ sparse.diags(idf)
|
| 212 |
+
Xu = sparse.diags(1 / (np.sqrt(np.asarray(Xu.multiply(Xu).sum(1)).ravel()) + 1e-6)) @ Xu
|
| 213 |
+
uu = R['u'].to_numpy().astype(np.int64); iic = R['ii'].to_numpy().astype(np.int64)
|
| 214 |
+
ex = X.select('r', 'j', 'pos').unique(['r', 'j', 'pos'])
|
| 215 |
+
# per-event history (last 20 clicks) is shared by all candidates: take rows of one candidate per (r) anyway
|
| 216 |
+
er_r = ex['r'].to_numpy(); er_j = ex['j'].to_numpy().astype(np.int64); er_w = (1.0 / np.sqrt(ex['pos'].to_numpy())).astype(np.float32)
|
| 217 |
+
er_l5 = ex['pos'].to_numpy() <= 5
|
| 218 |
+
nR = R.height
|
| 219 |
+
svcols = {}
|
| 220 |
+
for k in (16, 32, 64):
|
| 221 |
+
U, S, Vt = randomized_svd(Xc, k, random_state=0, n_iter=5)
|
| 222 |
+
V = (Vt.T * np.sqrt(S)).astype(np.float32); Uf = np.asarray(Xu @ (Vt.T / np.sqrt(S))).astype(np.float32) # fold-in = U*sqrt(S) for train users
|
| 223 |
+
Vn = V / (np.linalg.norm(V, axis=1, keepdims=True) + 1e-9); Un = Uf / (np.linalg.norm(Uf, axis=1, keepdims=True) + 1e-9)
|
| 224 |
+
cand = Vn[iic]
|
| 225 |
+
svcols[f'sv_k{k}_ucos'] = (Un[uu] * cand).sum(1)
|
| 226 |
+
svcols[f'sv_k{k}_udot'] = (Uf[uu] * V[iic]).sum(1)
|
| 227 |
+
hv = np.zeros((nR, k), np.float32); np.add.at(hv, er_r, Vn[er_j] * er_w[:, None])
|
| 228 |
+
hn = np.linalg.norm(hv, axis=1); svcols[f'sv_k{k}_hcos'] = (hv * cand).sum(1) / (hn + 1e-9)
|
| 229 |
+
if k == 32:
|
| 230 |
+
svcols['sv_k32_hnorm'] = hn
|
| 231 |
+
sims = (Vn[er_j] * Vn[iic[er_r]]).sum(1)
|
| 232 |
+
mx = np.full(nR, -1.0, np.float32); np.maximum.at(mx, er_r, sims)
|
| 233 |
+
mx5 = np.full(nR, -1.0, np.float32); np.maximum.at(mx5, er_r[er_l5], sims[er_l5])
|
| 234 |
+
svcols['sv_k32_imax20'] = mx; svcols['sv_k32_imax5'] = mx5
|
| 235 |
+
if k == 64:
|
| 236 |
+
# click centroid vs non-clicked-impression centroid (impressions frozen at C: P_rows only)
|
| 237 |
+
Sc = sparse.csr_matrix((np.ones(cnt.height, np.float32), (cnt['u'].to_numpy(), cnt['ii'].to_numpy())), shape=(NU, NI))
|
| 238 |
+
neg = NEGsrc.filter(pl.col('clicked') == 0)
|
| 239 |
+
Sn = sparse.csr_matrix((np.ones(neg.height, np.float32), (neg['u'].to_numpy(), neg['ii'].to_numpy())), shape=(NU, NI))
|
| 240 |
+
nC = np.asarray(Sc @ Vn); nC /= np.linalg.norm(nC, axis=1, keepdims=True) + 1e-9
|
| 241 |
+
nN = np.asarray(Sn @ Vn); nN /= np.linalg.norm(nN, axis=1, keepdims=True) + 1e-9
|
| 242 |
+
cc = (nC[uu] * cand).sum(1); cn = (nN[uu] * cand).sum(1)
|
| 243 |
+
svcols['sv_k64_ccos'] = cc; svcols['sv_k64_ncos'] = cn
|
| 244 |
+
for a, nm in ((0.25, '025'), (0.5, '05'), (1.0, '1')):
|
| 245 |
+
M = nC - a * nN; M /= np.linalg.norm(M, axis=1, keepdims=True) + 1e-9
|
| 246 |
+
svcols[f'sv_k64_con{nm}'] = (M[uu] * cand).sum(1)
|
| 247 |
+
R = R.with_columns([pl.Series(c, v.astype(np.float32)) for c, v in svcols.items()])
|
| 248 |
+
|
| 249 |
+
# ---------------- MF ----------------
|
| 250 |
+
if with_mf:
|
| 251 |
+
mf = fit_mf(P_rows, NU, NI)
|
| 252 |
+
uu = R['u'].to_numpy().astype(np.int64); ii = R['ii'].to_numpy().astype(np.int64)
|
| 253 |
+
dot = (mf['P'][uu] * mf['Q'][ii]).sum(1)
|
| 254 |
+
R = R.with_columns(pl.Series('mf_score', mf['b'] + mf['bu'][uu] + mf['bi'][ii] + dot),
|
| 255 |
+
pl.Series('mf_dot', dot), pl.Series('mf_bi', mf['bi'][ii]))
|
| 256 |
+
|
| 257 |
+
fill0 = [c for c in R.columns if c.startswith(('it_imp', 'it_clk', 'it_n', 'us_n', 'hist_n', 'hist_imp', 'hist_noclk', 'co_')) and R[c].dtype != pl.List]
|
| 258 |
+
R = R.with_columns([pl.col(c).fill_null(0) for c in fill0])
|
| 259 |
+
R = R.with_columns([pl.col(c).fill_null(g) for c in ('it_ctr', 'it_ctr24', 'it_ctr6')] +
|
| 260 |
+
[pl.col(c).fill_null(1.0) for c in ('it_trend24', 'it_trend6', 'it_imptrend24')] +
|
| 261 |
+
[pl.col('us_ctr').fill_null(g), pl.col('it_imp_rate').fill_null(0), pl.col('us_repeat_rate').fill_null(0)])
|
| 262 |
+
|
| 263 |
+
# ---------------- event context ----------------
|
| 264 |
+
main = ['it_ctr', 'it_ctr24', 'it_imp', 'it_nclk_all', 'it_clk_dec2d', 'hist_n_all', 'hist_noclk',
|
| 265 |
+
'co_cos_max20', 'co_cos_rw', 'co_tr_max5', 'co_tr_last',
|
| 266 |
+
'sv_k16_ucos', 'sv_k32_ucos', 'sv_k64_ucos', 'sv_k32_hcos', 'sv_k64_hcos', 'sv_k32_imax20', 'sv_k64_con05', 'sv_k64_con025', 'sv_k64_ccos'] + (['mf_score', 'mf_dot'] if with_mf else [])
|
| 267 |
+
exprs = []
|
| 268 |
+
for c in main:
|
| 269 |
+
x = pl.col(c).cast(pl.Float64)
|
| 270 |
+
exprs += [x.rank('average').over('eid').alias(f'ev_{c}_rk'),
|
| 271 |
+
((x - x.mean().over('eid')) / (x.std().over('eid') + 1e-9)).alias(f'ev_{c}_z'),
|
| 272 |
+
x.max().over('eid').alias(f'ev_{c}_max'), x.mean().over('eid').alias(f'ev_{c}_mean')]
|
| 273 |
+
R = R.with_columns(exprs)
|
| 274 |
+
R = R.drop([c for c in R.columns if c.startswith('_')] + ['h', 'h_ts', 'tb', 'dt']).sort('r').drop('r')
|
| 275 |
+
log(f'build C={C} rows={R.height} cols={R.width} {time.time()-t0:.0f}s')
|
| 276 |
+
return R
|
| 277 |
+
|
| 278 |
+
GROUPS = ['it', 'hist', 'us', 'co', 'ev', 'tm', 'sv', 'mf']
|
| 279 |
+
|
| 280 |
+
def feature_cols(df, groups):
|
| 281 |
+
cols = []
|
| 282 |
+
for c in df.columns:
|
| 283 |
+
if c in ID_COLS: continue
|
| 284 |
+
gname = c.split('_')[0]
|
| 285 |
+
if gname == 'ev':
|
| 286 |
+
src = c[3:].split('_')[0] # ev_<src group>_...
|
| 287 |
+
if 'ev' in groups and src in groups: cols.append(c)
|
| 288 |
+
elif gname in groups:
|
| 289 |
+
cols.append(c)
|
| 290 |
+
return cols
|
parallel/src_snapshot/v2work/v3fold.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""v3 features (v2work/v3feat_ne.py = v3/features.py with global/item stats from train data only) for all fold specs.
|
| 2 |
+
Usage: python v3fold.py NAME... -> v2work/feat_v3/NAME.parquet
|
| 3 |
+
f1..f6: first event/user in [Ck, Ck+1d); val25/val26: [C, end of train); eval: Jan27 cutoff
|
| 4 |
+
e1..e4: [Ck, Jan25) (eval-like staleness, for cutoff-25 validation); E1..E6: [Ck, Jan27) (eval-like, final fit)"""
|
| 5 |
+
import sys, os
|
| 6 |
+
sys.path.insert(0, '/workspace/huawei/v2work'); sys.path.insert(1, '/workspace/huawei/v3')
|
| 7 |
+
from v3feat_ne import load, build, log
|
| 8 |
+
from split import J20, DAY, first_events
|
| 9 |
+
TE, TR, EE, ER, NI, NU = load('/workspace/huawei/v3/data')
|
| 10 |
+
OUT = '/workspace/huawei/v2work/feat_v3'; os.makedirs(OUT, exist_ok=True)
|
| 11 |
+
SPEC = {**{f'f{k}': (J20 + k * DAY, DAY) for k in range(1, 7)}, 'val25': (J20 + 5 * DAY, None), 'val26': (J20 + 6 * DAY, None),
|
| 12 |
+
**{f'e{k}': (J20 + k * DAY, (5 - k) * DAY) for k in range(1, 5)}, **{f'E{k}': (J20 + k * DAY, (7 - k) * DAY) for k in range(1, 7)}}
|
| 13 |
+
for n in sys.argv[1:]:
|
| 14 |
+
if n == 'eval': R = build(J20 + 7 * DAY, EE, ER, TE, TR, NI, NU)
|
| 15 |
+
else:
|
| 16 |
+
C, W = SPEC[n]; T_ev = first_events(TE, C, W)
|
| 17 |
+
R = build(C, T_ev, TR.join(T_ev.select('eid'), on='eid', how='semi'), TE, TR, NI, NU)
|
| 18 |
+
R.write_parquet(f'{OUT}/{n}.parquet'); log('wrote', n, R.shape)
|
parallel/src_snapshot/v2work/validate.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Standalone validator: python validate.py SUB.csv"""
|
| 2 |
+
import sys, csv, math, numpy as np, polars as pl
|
| 3 |
+
out=sys.argv[1]
|
| 4 |
+
ids=pl.read_csv('/workspace/huawei/v2work/evaluation.csv',columns=['rowid'],schema_overrides={'rowid':pl.Utf8})['rowid'].to_list()
|
| 5 |
+
raw=open(out,'rb').read(); errs=[]
|
| 6 |
+
try: raw.decode('utf-8')
|
| 7 |
+
except Exception: errs.append('not utf-8')
|
| 8 |
+
with open(out,newline='',encoding='utf-8') as f:
|
| 9 |
+
r=csv.reader(f); h=next(r); rows=list(r)
|
| 10 |
+
if h!=['rowid','predicted_probability']: errs.append(f'header {h}')
|
| 11 |
+
if len(rows)!=529390: errs.append(f'rows {len(rows)}')
|
| 12 |
+
sid=[x[0] for x in rows]
|
| 13 |
+
if len(set(sid))!=len(sid): errs.append('dup rowids')
|
| 14 |
+
if set(sid)!=set(ids): errs.append('rowid set mismatch')
|
| 15 |
+
v=np.array([float(x[1]) for x in rows])
|
| 16 |
+
if not (np.isfinite(v).all() and (v>=0).all() and (v<=1).all()): errs.append('bad values')
|
| 17 |
+
print(f'rows={len(rows)} unique={len(set(sid))} min={v.min():.4g} mean={v.mean():.5g} max={v.max():.4g} distinct={len(np.unique(v))}')
|
| 18 |
+
print('VALID' if not errs else 'INVALID '+';'.join(errs))
|
parallel/src_snapshot/v2work/write_sub.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Write + validate submission from eval_pred.parquet. Usage: python write_sub.py OUT.csv"""
|
| 2 |
+
import sys, csv, math, hashlib, zipfile, os, numpy as np, polars as pl
|
| 3 |
+
out=sys.argv[1]
|
| 4 |
+
pr=pl.read_parquet(os.environ.get('PRED','/workspace/huawei/v2work/eval_pred.parquet'))
|
| 5 |
+
ev=pl.read_csv('/workspace/huawei/v2work/evaluation.csv',columns=['rowid'],schema_overrides={'rowid':pl.Utf8})
|
| 6 |
+
df=ev.join(pr,on='rowid',how='left',maintain_order='left')
|
| 7 |
+
p=np.nan_to_num(df['p'].to_numpy().astype(np.float64),nan=0.0629,posinf=1.0,neginf=0.0).clip(0,1)
|
| 8 |
+
p[np.isnan(df['p'].to_numpy().astype(float))]=0.0629
|
| 9 |
+
print('missing preds filled:',int(df['p'].null_count()))
|
| 10 |
+
with open(out,'w',encoding='utf-8',newline='') as f:
|
| 11 |
+
f.write('rowid,predicted_probability\n'); f.writelines(f'{r},{v:.8g}\n' for r,v in zip(df['rowid'].to_list(),p))
|
| 12 |
+
# ---- validator
|
| 13 |
+
ids=ev['rowid'].to_list(); errs=[]
|
| 14 |
+
with open(out,newline='',encoding='utf-8') as f:
|
| 15 |
+
r=csv.reader(f); h=next(r); rows=list(r)
|
| 16 |
+
if h!=['rowid','predicted_probability']: errs.append('header')
|
| 17 |
+
if len(rows)!=529390 or len(ids)!=529390: errs.append(f'rows {len(rows)}')
|
| 18 |
+
sid=[x[0] for x in rows]
|
| 19 |
+
if len(set(sid))!=len(sid) or set(sid)!=set(ids): errs.append('rowid set mismatch')
|
| 20 |
+
vals=[float(x[1]) for x in rows]
|
| 21 |
+
if not all(math.isfinite(v) and 0<=v<=1 for v in vals): errs.append('bad values')
|
| 22 |
+
v=np.array(vals); print(f'rows={len(rows)} unique={len(set(sid))} min={v.min():.4g} mean={v.mean():.4g} max={v.max():.4g} distinct={len(np.unique(v))}')
|
| 23 |
+
print('VALID' if not errs else 'INVALID '+';'.join(errs))
|
| 24 |
+
zp=out+'.zip'
|
| 25 |
+
with zipfile.ZipFile(zp,'w',zipfile.ZIP_DEFLATED) as z: z.write(out,arcname=os.path.basename(out))
|
| 26 |
+
with zipfile.ZipFile(zp) as z: print('zip contents',z.namelist())
|
| 27 |
+
print('sha256',hashlib.sha256(open(out,'rb').read()).hexdigest())
|
parallel/src_snapshot/v3/ablation.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Ablation: add feature groups one at a time; binary LightGBM; val AUC at both cutoffs. Writes out/ablation.json."""
|
| 2 |
+
import json, os, sys, time
|
| 3 |
+
from train import load_table, fit, FOLDS
|
| 4 |
+
from features import feature_cols
|
| 5 |
+
D = os.path.dirname(os.path.abspath(__file__))
|
| 6 |
+
LR = float(os.environ.get('ABL_LR', '0.1')); TH = int(os.environ.get('ABL_THREADS', '4'))
|
| 7 |
+
steps = [['it'], ['it', 'hist'], ['it', 'hist', 'us'], ['it', 'hist', 'us', 'co'], ['it', 'hist', 'us', 'co', 'sv'],
|
| 8 |
+
['it', 'hist', 'us', 'co', 'sv', 'mf'], ['it', 'hist', 'us', 'co', 'sv', 'mf', 'ev'],
|
| 9 |
+
['it', 'hist', 'us', 'co', 'sv', 'mf', 'ev', 'tm']]
|
| 10 |
+
out = f'{D}/out/ablation.json'
|
| 11 |
+
res = json.load(open(out)) if os.path.exists(out) else {}
|
| 12 |
+
for cut in (25, 26):
|
| 13 |
+
tr = load_table(FOLDS[cut]); va = load_table([f'val{cut}'])
|
| 14 |
+
for g in steps:
|
| 15 |
+
key = f'{cut}:' + '+'.join(g)
|
| 16 |
+
if key in res: continue
|
| 17 |
+
t0 = time.time()
|
| 18 |
+
feats = feature_cols(tr, g)
|
| 19 |
+
m, best, auc = fit(tr, va, feats, 'binary', LR, TH)
|
| 20 |
+
res[key] = dict(auc=auc, best_iter=best, n_feats=len(feats), secs=round(time.time() - t0))
|
| 21 |
+
print(key, res[key], flush=True)
|
| 22 |
+
json.dump(res, open(out, 'w'), indent=1)
|
parallel/src_snapshot/v3/baselines.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Single-score baselines on the local validation sets (val25 / val26), incl. a faithful port of starter_solution.py
|
| 2 |
+
fitted only on events < cutoff. Appends to out/baselines.json."""
|
| 3 |
+
import json, os, sys
|
| 4 |
+
import numpy as np, polars as pl
|
| 5 |
+
from features import load
|
| 6 |
+
from split import C_MAIN, C_SECOND, split
|
| 7 |
+
from metric import global_auc
|
| 8 |
+
D = os.path.dirname(os.path.abspath(__file__))
|
| 9 |
+
TE, TR, EE, ER, NI, NU = load(f'{D}/data')
|
| 10 |
+
K = 50; CFW = 25.0
|
| 11 |
+
lg = lambda p: np.log(np.clip(p, 1e-6, 1 - 1e-6) / (1 - np.clip(p, 1e-6, 1 - 1e-6)))
|
| 12 |
+
res = {}
|
| 13 |
+
for tag, C in ((25, C_MAIN), (26, C_SECOND)):
|
| 14 |
+
fe, fr, ve, vr = split(TE, TR, C)
|
| 15 |
+
P = fr.join(fe.select('eid', 'u'), on='eid')
|
| 16 |
+
g = P['clicked'].mean()
|
| 17 |
+
ic = P.group_by('ii').agg(((pl.col('clicked').sum() + K * g) / (pl.len() + K)).alias('ictr'))
|
| 18 |
+
uc = P.group_by('u').agg(((pl.col('clicked').sum() + K * g) / (pl.len() + K)).alias('uctr'))
|
| 19 |
+
inter = pl.concat([fe.select('u', 'h').explode('h').drop_nulls().rename({'h': 'ii'}).with_columns(pl.col('ii').cast(pl.Int32)),
|
| 20 |
+
P.filter(pl.col('clicked') == 1).select('u', pl.col('ii').cast(pl.Int32))]).unique()
|
| 21 |
+
pop = inter.group_by('ii').agg(pl.len().alias('pop'))
|
| 22 |
+
pairs = (inter.join(inter, on='u', suffix='_b').filter(pl.col('ii') != pl.col('ii_b')).group_by('ii', 'ii_b').agg(pl.len().alias('c'))
|
| 23 |
+
.join(pop, on='ii').join(pop.rename({'ii': 'ii_b', 'pop': 'pop_b'}), on='ii_b')
|
| 24 |
+
.with_columns((pl.col('c') / (pl.col('pop') * pl.col('pop_b')).sqrt()).alias('cs')).select('ii', 'ii_b', 'cs'))
|
| 25 |
+
R = vr.join(ve.select('eid', 'u', 'h'), on='eid').with_row_index('r')
|
| 26 |
+
PR = R.select('r', pl.col('ii').cast(pl.Int32), 'h').explode('h').drop_nulls('h').rename({'h': 'ii_b'}).with_columns(pl.col('ii_b').cast(pl.Int32))
|
| 27 |
+
cf = PR.join(pairs, on=['ii', 'ii_b'], how='left').group_by('r').agg(pl.col('cs').fill_null(0).sum().alias('cf'))
|
| 28 |
+
R = (R.join(ic, on='ii', how='left').join(uc, on='u', how='left').join(cf, on='r', how='left')
|
| 29 |
+
.with_columns(pl.col('ictr').fill_null(g), pl.col('uctr').fill_null(g), pl.col('cf').fill_null(0)).sort('r'))
|
| 30 |
+
y = R['clicked'].to_numpy()
|
| 31 |
+
starter = lg(R['ictr'].to_numpy()) + lg(R['uctr'].to_numpy()) - lg(g) + CFW * R['cf'].to_numpy()
|
| 32 |
+
F = pl.read_parquet(f'{D}/feat/val{tag}.parquet')
|
| 33 |
+
yf = F['clicked'].to_numpy()
|
| 34 |
+
res[tag] = dict(starter=global_auc(y, starter), starter_item_ctr_k50=global_auc(y, R['ictr'].to_numpy()),
|
| 35 |
+
item_ctr_smoothed=global_auc(yf, F['it_ctr'].to_numpy()), item_impressions=global_auc(yf, F['it_imp'].to_numpy()),
|
| 36 |
+
cand_in_history=global_auc(yf, F['hist_in'].to_numpy()), n_rows=len(y), n_events=ve.height, ctr=float(y.mean()))
|
| 37 |
+
print(tag, res[tag], flush=True)
|
| 38 |
+
json.dump(res, open(f'{D}/out/baselines.json', 'w'), indent=1)
|
parallel/src_snapshot/v3/blend.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Rank-average of submission_v1 (external percentile ranks) and submission_v2. Not measurable on local validation.
|
| 2 |
+
Usage: python blend.py [w_v2=0.5]"""
|
| 3 |
+
import sys, numpy as np, polars as pl
|
| 4 |
+
w = float(sys.argv[1]) if len(sys.argv) > 1 else 0.5
|
| 5 |
+
rd = lambda p: pl.read_csv(p, schema_overrides={'rowid': pl.Utf8, 'predicted_probability': pl.Float64})
|
| 6 |
+
ex = rd('/workspace/huawei/example_submission.csv').select('rowid')
|
| 7 |
+
v1 = rd('/workspace/huawei/submission_v1.csv'); v2 = rd('/workspace/huawei/submission_v2.csv')
|
| 8 |
+
J = ex.join(v1.rename({'predicted_probability': 'p1'}), on='rowid', how='left', maintain_order='left') \
|
| 9 |
+
.join(v2.rename({'predicted_probability': 'p2'}), on='rowid', how='left', maintain_order='left')
|
| 10 |
+
assert J.height == ex.height and J['p1'].null_count() == 0 and J['p2'].null_count() == 0
|
| 11 |
+
n = J.height
|
| 12 |
+
r1 = J['p1'].rank('average').to_numpy() / n; r2 = J['p2'].rank('average').to_numpy() / n
|
| 13 |
+
p = np.clip(np.nan_to_num((1 - w) * r1 + w * r2, nan=0.0629), 0, 1)
|
| 14 |
+
out = '/workspace/huawei/v3/submission_blend.csv'
|
| 15 |
+
pl.DataFrame({'rowid': J['rowid'], 'predicted_probability': p}).to_pandas().to_csv(out, index=False, float_format='%.8g')
|
| 16 |
+
print('wrote', out, 'w_v2', w)
|
parallel/src_snapshot/v3/build_feats.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Build & cache feature tables. Usage: python build_feats.py [val|final|all]
|
| 2 |
+
folds f1..f6 : cutoff Ck=Jan20+k d, targets = first event/user in [Ck, Ck+1d) (model-training rows)
|
| 3 |
+
val25 / val26: cutoff Jan25 / Jan26, targets = first event/user at/after cutoff (to end of train)
|
| 4 |
+
eval : cutoff Jan27, targets = evaluation events
|
| 5 |
+
"""
|
| 6 |
+
import os, sys, time
|
| 7 |
+
import polars as pl
|
| 8 |
+
from features import load, build, log
|
| 9 |
+
from split import J20, DAY, first_events
|
| 10 |
+
D = os.path.dirname(os.path.abspath(__file__))
|
| 11 |
+
TE, TR, EE, ER, NI, NU = load(f'{D}/data')
|
| 12 |
+
mode = sys.argv[1] if len(sys.argv) > 1 else 'all'
|
| 13 |
+
specs = []
|
| 14 |
+
if mode in ('val', 'all'):
|
| 15 |
+
specs += [(f'f{k}', J20 + k * DAY, DAY) for k in range(1, 6)] + [('val25', J20 + 5 * DAY, None), ('val26', J20 + 6 * DAY, None)]
|
| 16 |
+
if mode in ('final', 'all'):
|
| 17 |
+
specs += [s for s in [('f6', J20 + 6 * DAY, DAY), ('eval', J20 + 7 * DAY, 'eval')] if s[0] not in [x[0] for x in specs]]
|
| 18 |
+
t0 = time.time()
|
| 19 |
+
for name, C, W in specs:
|
| 20 |
+
out = f'{D}/feat/{name}.parquet'
|
| 21 |
+
if os.path.exists(out) and '--force' not in sys.argv:
|
| 22 |
+
log('skip', name); continue
|
| 23 |
+
if W == 'eval':
|
| 24 |
+
R = build(C, EE, ER, TE, TR, NI, NU)
|
| 25 |
+
else:
|
| 26 |
+
T_ev = first_events(TE, C, W)
|
| 27 |
+
R = build(C, T_ev, TR.join(T_ev.select('eid'), on='eid', how='semi'), TE, TR, NI, NU)
|
| 28 |
+
R.write_parquet(out)
|
| 29 |
+
log(f'feature build total {time.time()-t0:.0f}s')
|
parallel/src_snapshot/v3/features.py
ADDED
|
@@ -0,0 +1,285 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Feature builder (as-of-cutoff, leakage-safe) for TechArena CTR.
|
| 2 |
+
|
| 3 |
+
build(C, T_ev, T_rows, TE, TR) returns one feature row per target candidate.
|
| 4 |
+
Every statistic uses ONLY:
|
| 5 |
+
* train events/rows with ts < C (fit window: impressions + labels), and
|
| 6 |
+
* click histories of events with ts < C, plus the target event's OWN history (strictly before its ts).
|
| 7 |
+
Since target events are each user's first event at/after C, their histories contain only clicks < C,
|
| 8 |
+
so nothing observed after a target row's own event time is ever joined.
|
| 9 |
+
|
| 10 |
+
Feature groups (column prefixes, used for ablation):
|
| 11 |
+
hist_ candidate vs the user's click log / own history / past impressions of that user-item pair
|
| 12 |
+
it_ item popularity & CTR (full fit window, last 24h, last 6h, trend)
|
| 13 |
+
co_ item-item co-click (user co-occurrence cosine) and sequential transitions vs user's last 5/20 clicks
|
| 14 |
+
us_ user activity / CTR / recency / cold-start flags
|
| 15 |
+
ev_ within-event rank / z-score / event max & mean of the main features
|
| 16 |
+
tm_ time_bucket, day_type, hour, staleness since cutoff
|
| 17 |
+
sv_ truncated SVD (tf-idf, row-normalised user x item click matrix as of C): user/history/item cosines
|
| 18 |
+
mf_ logistic matrix factorisation score fitted on fit-window impressions
|
| 19 |
+
"""
|
| 20 |
+
import time
|
| 21 |
+
import numpy as np
|
| 22 |
+
import polars as pl
|
| 23 |
+
from scipy import sparse
|
| 24 |
+
from sklearn.utils.extmath import randomized_svd
|
| 25 |
+
|
| 26 |
+
J20 = 1737331200
|
| 27 |
+
DAY = 86400
|
| 28 |
+
G_PRIOR = 0.0629
|
| 29 |
+
K_ITEM = 20 # pseudo-impressions for item CTR shrinkage
|
| 30 |
+
K_USER = 5 # pseudo-events for user CTR shrinkage
|
| 31 |
+
TOPK = 200 # co-click neighbours kept per item
|
| 32 |
+
ID_COLS = ['rowid', 'eid', 'u', 'ii', 'ts', 'clicked', 'ord']
|
| 33 |
+
_t0 = time.time()
|
| 34 |
+
|
| 35 |
+
def log(*a):
|
| 36 |
+
print(time.strftime('%H:%M:%S'), *a, flush=True)
|
| 37 |
+
|
| 38 |
+
def load(D):
|
| 39 |
+
TE = pl.read_parquet(f'{D}/ev_train.parquet')
|
| 40 |
+
TR = pl.read_parquet(f'{D}/rows_train.parquet')
|
| 41 |
+
EE = pl.read_parquet(f'{D}/ev_eval.parquet')
|
| 42 |
+
ER = pl.read_parquet(f'{D}/rows_eval.parquet')
|
| 43 |
+
NI = pl.read_parquet(f'{D}/items.parquet').height
|
| 44 |
+
NU = pl.read_parquet(f'{D}/users.parquet').height
|
| 45 |
+
return TE, TR, EE, ER, NI, NU
|
| 46 |
+
|
| 47 |
+
def _hist_long(E):
|
| 48 |
+
return (E.select('u', 'h', 'h_ts').explode(['h', 'h_ts']).drop_nulls('h')
|
| 49 |
+
.select('u', pl.col('h').cast(pl.Int32).alias('ii'), pl.col('h_ts').cast(pl.Int64).alias('ts')))
|
| 50 |
+
|
| 51 |
+
def click_log(C, P_ev, P_rows, T_ev):
|
| 52 |
+
"""Exact (user,item,ts) click log as of C: fit-window clicks + warm-up clicks seen in pre-C / own histories."""
|
| 53 |
+
win = P_rows.filter(pl.col('clicked') == 1).select('u', pl.col('ii').cast(pl.Int32), 'ts')
|
| 54 |
+
hist = pl.concat([_hist_long(P_ev), _hist_long(T_ev)]).filter(pl.col('ts') < C)
|
| 55 |
+
CL = pl.concat([win, hist]).unique().sort('u', 'ts', 'ii')
|
| 56 |
+
return CL
|
| 57 |
+
|
| 58 |
+
def cooc_tables(CL, NI):
|
| 59 |
+
"""User co-occurrence cosine (top-K per item) + lag-weighted transition probabilities prev->next."""
|
| 60 |
+
us = CL.select('u', 'ii').unique()
|
| 61 |
+
pop = us.group_by('ii').agg(pl.len().alias('pop'))
|
| 62 |
+
pairs = (us.join(us, on='u', suffix='_b').filter(pl.col('ii') != pl.col('ii_b'))
|
| 63 |
+
.group_by('ii', 'ii_b').agg(pl.len().cast(pl.Int32).alias('co')))
|
| 64 |
+
co = (pairs.join(pop, on='ii').join(pop.rename({'ii': 'ii_b', 'pop': 'pop_b'}), on='ii_b')
|
| 65 |
+
.with_columns((pl.col('co') / (pl.col('pop') * pl.col('pop_b')).sqrt()).cast(pl.Float32).alias('cos'))
|
| 66 |
+
.select(pl.col('ii_b').alias('j'), 'ii', 'cos') # j = history item, ii = candidate
|
| 67 |
+
.sort('cos', descending=True).group_by('j').head(TOPK))
|
| 68 |
+
seq = CL.select('u', 'ii') # already sorted by u, ts
|
| 69 |
+
parts = []
|
| 70 |
+
for lag in (1, 2, 3):
|
| 71 |
+
parts.append(seq.with_columns(pl.col('ii').shift(lag).over('u').alias('j')).drop_nulls('j')
|
| 72 |
+
.select(pl.col('j').cast(pl.Int32), 'ii', pl.lit(1.0 / lag).alias('w')))
|
| 73 |
+
trn = pl.concat(parts).filter(pl.col('j') != pl.col('ii')).group_by('j', 'ii').agg(pl.col('w').sum())
|
| 74 |
+
tot = trn.group_by('j').agg(pl.col('w').sum().alias('tot'))
|
| 75 |
+
trn = (trn.join(tot, on='j').with_columns((pl.col('w') / (pl.col('tot') + 5.0)).cast(pl.Float32).alias('trp'))
|
| 76 |
+
.select('j', 'ii', 'trp').sort('trp', descending=True).group_by('j').head(TOPK))
|
| 77 |
+
return co, trn
|
| 78 |
+
|
| 79 |
+
def fit_mf(P_rows, NU, NI, dim=24, epochs=6, lr=0.05, l2=1e-4, seed=0, batch=200_000):
|
| 80 |
+
"""Logistic MF logit = b + bu + bi + <pu, qi> fitted with Adagrad on fit-window impressions (numpy)."""
|
| 81 |
+
rng = np.random.default_rng(seed)
|
| 82 |
+
u = P_rows['u'].to_numpy().astype(np.int64); i = P_rows['ii'].to_numpy().astype(np.int64)
|
| 83 |
+
y = P_rows['clicked'].to_numpy().astype(np.float32)
|
| 84 |
+
P = (rng.standard_normal((NU, dim)) * 0.01).astype(np.float32); Q = (rng.standard_normal((NI, dim)) * 0.01).astype(np.float32)
|
| 85 |
+
bu = np.zeros(NU, np.float32); bi = np.zeros(NI, np.float32)
|
| 86 |
+
b = np.float32(np.log(y.mean() / (1 - y.mean())))
|
| 87 |
+
gP = np.full_like(P, 1e-6); gQ = np.full_like(Q, 1e-6); gbu = np.full_like(bu, 1e-6); gbi = np.full_like(bi, 1e-6)
|
| 88 |
+
n = len(y)
|
| 89 |
+
for ep in range(epochs):
|
| 90 |
+
perm = rng.permutation(n)
|
| 91 |
+
for s in range(0, n, batch):
|
| 92 |
+
idx = perm[s:s + batch]; ub, ib, yb = u[idx], i[idx], y[idx]
|
| 93 |
+
pu, qi = P[ub], Q[ib]
|
| 94 |
+
z = b + bu[ub] + bi[ib] + (pu * qi).sum(1)
|
| 95 |
+
g = (1 / (1 + np.exp(-z)) - yb).astype(np.float32) # dL/dz
|
| 96 |
+
m = len(idx)
|
| 97 |
+
Su = sparse.csr_matrix((np.ones(m, np.float32), (ub, np.arange(m))), shape=(NU, m))
|
| 98 |
+
Si = sparse.csr_matrix((np.ones(m, np.float32), (ib, np.arange(m))), shape=(NI, m))
|
| 99 |
+
dP = Su @ (g[:, None] * qi) + l2 * P * (np.asarray(Su.sum(1)).ravel() > 0)[:, None]
|
| 100 |
+
dQ = Si @ (g[:, None] * pu) + l2 * Q * (np.asarray(Si.sum(1)).ravel() > 0)[:, None]
|
| 101 |
+
dbu = Su @ g; dbi = Si @ g
|
| 102 |
+
gP += dP * dP; gQ += dQ * dQ; gbu += dbu * dbu; gbi += dbi * dbi
|
| 103 |
+
P -= lr * dP / np.sqrt(gP); Q -= lr * dQ / np.sqrt(gQ)
|
| 104 |
+
bu -= lr * dbu / np.sqrt(gbu); bi -= lr * dbi / np.sqrt(gbi)
|
| 105 |
+
return dict(b=b, bu=bu, bi=bi, P=P, Q=Q)
|
| 106 |
+
|
| 107 |
+
def build(C, T_ev, T_rows, TE, TR, NI, NU, with_mf=True, sv_mode='std'):
|
| 108 |
+
t0 = time.time()
|
| 109 |
+
P_ev = TE.filter(pl.col('ts') < C)
|
| 110 |
+
P_rows = TR.join(P_ev.select('eid', 'u', 'ts'), on='eid', how='inner')
|
| 111 |
+
g = float(P_rows['clicked'].mean())
|
| 112 |
+
ndays = (C - J20) / DAY
|
| 113 |
+
CL = click_log(C, P_ev, P_rows, T_ev)
|
| 114 |
+
|
| 115 |
+
# ---------------- item ----------------
|
| 116 |
+
def ictr(c, n):
|
| 117 |
+
return (pl.col(c) + K_ITEM * g) / (pl.col(n) + K_ITEM)
|
| 118 |
+
it = P_rows.group_by('ii').agg(
|
| 119 |
+
pl.len().alias('it_imp'), pl.col('clicked').sum().cast(pl.Int32).alias('it_clk'),
|
| 120 |
+
(pl.col('ts') >= C - DAY).sum().alias('it_imp24'), pl.col('clicked').filter(pl.col('ts') >= C - DAY).sum().cast(pl.Int32).alias('it_clk24'),
|
| 121 |
+
(pl.col('ts') >= C - 6 * 3600).sum().alias('it_imp6'), pl.col('clicked').filter(pl.col('ts') >= C - 6 * 3600).sum().cast(pl.Int32).alias('it_clk6'))
|
| 122 |
+
it = it.with_columns(ictr('it_clk', 'it_imp').alias('it_ctr'), ictr('it_clk24', 'it_imp24').alias('it_ctr24'),
|
| 123 |
+
ictr('it_clk6', 'it_imp6').alias('it_ctr6'))
|
| 124 |
+
it = it.with_columns((pl.col('it_ctr24') / pl.col('it_ctr')).alias('it_trend24'), (pl.col('it_ctr6') / pl.col('it_ctr')).alias('it_trend6'),
|
| 125 |
+
(pl.col('it_imp24') / (pl.col('it_imp') / ndays)).alias('it_imptrend24'),
|
| 126 |
+
(pl.col('it_imp') / ndays).alias('it_imp_rate'))
|
| 127 |
+
ip = CL.group_by('ii').agg(pl.len().alias('it_nclk_all'), pl.col('u').n_unique().alias('it_nusers'),
|
| 128 |
+
(pl.col('ts') < J20).sum().alias('it_nclk_warm'),
|
| 129 |
+
((pl.col('ts') - C) / (2 * DAY)).exp().sum().cast(pl.Float32).alias('it_clk_dec2d'))
|
| 130 |
+
|
| 131 |
+
# ---------------- user ----------------
|
| 132 |
+
ue = P_rows.group_by('u').agg(pl.col('eid').n_unique().alias('us_nev'), pl.col('clicked').sum().cast(pl.Int32).alias('us_nclk_win'),
|
| 133 |
+
pl.col('ts').max().alias('_u_last_ev'))
|
| 134 |
+
ue = ue.with_columns(((pl.col('us_nclk_win') + K_USER * 0.629) / (pl.col('us_nev') + K_USER)).alias('us_ctr'))
|
| 135 |
+
uc = CL.group_by('u').agg(pl.len().alias('us_nclk_all'), (pl.col('ts') < J20).sum().alias('us_nclk_warm'),
|
| 136 |
+
pl.col('ii').n_unique().alias('us_nuniq'), pl.col('ts').max().alias('_u_last_clk'),
|
| 137 |
+
(pl.col('ts') >= C - DAY).sum().alias('us_nclk24'))
|
| 138 |
+
uc = uc.with_columns((1 - pl.col('us_nuniq') / pl.col('us_nclk_all')).alias('us_repeat_rate'))
|
| 139 |
+
|
| 140 |
+
# ---------------- user x item ----------------
|
| 141 |
+
uic = CL.group_by('u', 'ii').agg(pl.len().alias('hist_n_all'), (pl.col('ts') < J20).sum().alias('hist_n_warm'),
|
| 142 |
+
(pl.col('ts') >= J20).sum().alias('hist_n_win'), pl.col('ts').max().alias('_ui_last_clk'))
|
| 143 |
+
uii = P_rows.group_by('u', 'ii').agg(pl.len().alias('hist_imp'), pl.col('clicked').sum().cast(pl.Int32).alias('_ui_clk'),
|
| 144 |
+
pl.col('ts').max().alias('_ui_last_imp'))
|
| 145 |
+
uii = uii.with_columns((pl.col('hist_imp') - pl.col('_ui_clk')).alias('hist_noclk'))
|
| 146 |
+
|
| 147 |
+
# ---------------- assemble ----------------
|
| 148 |
+
R = (T_rows.select([c for c in ('rowid', 'eid', 'ii', 'clicked', 'ord') if c in T_rows.columns])
|
| 149 |
+
.join(T_ev.select('eid', 'u', 'ts', 'tb', 'dt', 'h', 'h_ts'), on='eid', how='left').with_row_index('r'))
|
| 150 |
+
R = (R.join(it, on='ii', how='left').join(ip, on='ii', how='left').join(ue, on='u', how='left').join(uc, on='u', how='left')
|
| 151 |
+
.join(uic, on=['u', 'ii'], how='left').join(uii, on=['u', 'ii'], how='left'))
|
| 152 |
+
R = R.with_columns(
|
| 153 |
+
pl.col('h').list.len().alias('us_hlen'),
|
| 154 |
+
(pl.col('h').list.len() == 0).cast(pl.Int8).alias('us_nohist'),
|
| 155 |
+
pl.col('us_nev').is_null().cast(pl.Int8).alias('us_cold'),
|
| 156 |
+
(pl.col('ts') - pl.col('_u_last_ev')).alias('us_since_ev'),
|
| 157 |
+
(pl.col('ts') - pl.col('_u_last_clk')).alias('us_since_clk'),
|
| 158 |
+
(pl.col('ts') - pl.col('_ui_last_clk')).alias('hist_since_clk'),
|
| 159 |
+
(pl.col('ts') - pl.col('_ui_last_imp')).alias('hist_since_imp'),
|
| 160 |
+
(pl.col('ts') - C).alias('tm_stale'), ((pl.col('ts') // 3600) % 24).cast(pl.Int8).alias('tm_hour'),
|
| 161 |
+
pl.col('tb').alias('tm_tb'), pl.col('dt').alias('tm_dt'))
|
| 162 |
+
# position (from end, 1 = most recent) of candidate in own history
|
| 163 |
+
hx = (R.select('r', 'ii', 'h').with_columns(pl.int_ranges(pl.col('h').list.len(), 0, -1).alias('pos'))
|
| 164 |
+
.explode(['h', 'pos']).drop_nulls('h').filter(pl.col('h') == pl.col('ii'))
|
| 165 |
+
.group_by('r').agg(pl.col('pos').min().alias('hist_pos'), pl.len().alias('hist_n_own')))
|
| 166 |
+
R = R.join(hx, on='r', how='left')
|
| 167 |
+
R = R.with_columns(pl.col('hist_since_clk').fill_null(30 * DAY), pl.col('hist_since_imp').fill_null(30 * DAY),
|
| 168 |
+
pl.col('hist_pos').fill_null(99), pl.col('hist_n_own').fill_null(0), pl.col('us_since_ev').fill_null(30 * DAY), pl.col('us_since_clk').fill_null(30 * DAY))
|
| 169 |
+
R = R.with_columns((pl.col('hist_n_all').fill_null(0) > 0).cast(pl.Int8).alias('hist_in'))
|
| 170 |
+
|
| 171 |
+
# ---------------- co-click ----------------
|
| 172 |
+
co, trn = cooc_tables(CL, NI)
|
| 173 |
+
last20 = CL.group_by('u', maintain_order=True).agg(pl.col('ii').tail(20).alias('l20'))
|
| 174 |
+
T_u = R.select('r', 'u', 'ii').join(last20, on='u', how='left')
|
| 175 |
+
X = (T_u.with_columns(pl.int_ranges(pl.col('l20').list.len(), 0, -1).alias('pos')).explode(['l20', 'pos'])
|
| 176 |
+
.drop_nulls('l20').rename({'l20': 'j'}).with_columns(pl.col('j').cast(pl.Int32))
|
| 177 |
+
.join(co, on=['j', 'ii'], how='left').join(trn, on=['j', 'ii'], how='left')
|
| 178 |
+
.with_columns(pl.col('cos').fill_null(0), pl.col('trp').fill_null(0)))
|
| 179 |
+
l5 = pl.col('pos') <= 5
|
| 180 |
+
agg = X.group_by('r').agg(
|
| 181 |
+
pl.col('cos').max().alias('co_cos_max20'), pl.col('cos').mean().alias('co_cos_mean20'),
|
| 182 |
+
pl.col('cos').filter(l5).max().alias('co_cos_max5'), pl.col('cos').filter(l5).mean().alias('co_cos_mean5'),
|
| 183 |
+
pl.col('cos').filter(pl.col('pos') == 1).first().alias('co_cos_last'),
|
| 184 |
+
pl.col('trp').max().alias('co_tr_max20'), pl.col('trp').mean().alias('co_tr_mean20'),
|
| 185 |
+
pl.col('trp').filter(l5).max().alias('co_tr_max5'), pl.col('trp').filter(l5).mean().alias('co_tr_mean5'),
|
| 186 |
+
pl.col('trp').filter(pl.col('pos') == 1).first().alias('co_tr_last'),
|
| 187 |
+
(pl.col('cos') / pl.col('pos').cast(pl.Float32).sqrt()).sum().alias('co_cos_rw'), pl.col('cos').sum().alias('co_cos_sum20'))
|
| 188 |
+
R = R.join(agg, on='r', how='left').sort('r')
|
| 189 |
+
|
| 190 |
+
# ---------------- SVD embeddings ----------------
|
| 191 |
+
# sv_mode (leakage checks only): 'std' = click log as of C; 'notgt' = drop target events' own histories;
|
| 192 |
+
# 'lag1d' = only clicks/impressions with ts < C - 1 day
|
| 193 |
+
if sv_mode == 'std':
|
| 194 |
+
CLsv, NEGsrc = CL, P_rows
|
| 195 |
+
elif sv_mode == 'notgt':
|
| 196 |
+
CLsv = pl.concat([P_rows.filter(pl.col('clicked') == 1).select('u', pl.col('ii').cast(pl.Int32), 'ts'),
|
| 197 |
+
_hist_long(P_ev).filter(pl.col('ts') < C)]).unique()
|
| 198 |
+
NEGsrc = P_rows
|
| 199 |
+
elif sv_mode == 'lag1d':
|
| 200 |
+
CLsv, NEGsrc = CL.filter(pl.col('ts') < C - DAY), P_rows.filter(pl.col('ts') < C - DAY)
|
| 201 |
+
else:
|
| 202 |
+
raise ValueError(sv_mode)
|
| 203 |
+
assert (CLsv['ts'].max() or 0) < C and (NEGsrc['ts'].max() or 0) < C
|
| 204 |
+
cnt = CLsv.group_by('u', 'ii').agg(pl.len().alias('n'))
|
| 205 |
+
Xc = sparse.csr_matrix((np.ones(cnt.height, np.float32), (cnt['u'].to_numpy(), cnt['ii'].to_numpy())), shape=(NU, NI))
|
| 206 |
+
df_ = np.asarray(Xc.sum(0)).ravel(); idf = np.log((NU + 1) / (df_ + 1)).astype(np.float32)
|
| 207 |
+
Xc = Xc @ sparse.diags(idf); rn = np.sqrt(np.asarray(Xc.multiply(Xc).sum(1)).ravel()) + 1e-6; Xc = sparse.diags(1 / rn) @ Xc
|
| 208 |
+
uu = R['u'].to_numpy().astype(np.int64); iic = R['ii'].to_numpy().astype(np.int64)
|
| 209 |
+
ex = X.select('r', 'j', 'pos').unique(['r', 'j', 'pos'])
|
| 210 |
+
# per-event history (last 20 clicks) is shared by all candidates: take rows of one candidate per (r) anyway
|
| 211 |
+
er_r = ex['r'].to_numpy(); er_j = ex['j'].to_numpy().astype(np.int64); er_w = (1.0 / np.sqrt(ex['pos'].to_numpy())).astype(np.float32)
|
| 212 |
+
er_l5 = ex['pos'].to_numpy() <= 5
|
| 213 |
+
nR = R.height
|
| 214 |
+
svcols = {}
|
| 215 |
+
for k in (16, 32, 64):
|
| 216 |
+
U, S, Vt = randomized_svd(Xc, k, random_state=0, n_iter=5)
|
| 217 |
+
V = (Vt.T * np.sqrt(S)).astype(np.float32); Uf = (U * np.sqrt(S)).astype(np.float32)
|
| 218 |
+
Vn = V / (np.linalg.norm(V, axis=1, keepdims=True) + 1e-9); Un = Uf / (np.linalg.norm(Uf, axis=1, keepdims=True) + 1e-9)
|
| 219 |
+
cand = Vn[iic]
|
| 220 |
+
svcols[f'sv_k{k}_ucos'] = (Un[uu] * cand).sum(1)
|
| 221 |
+
svcols[f'sv_k{k}_udot'] = (Uf[uu] * V[iic]).sum(1)
|
| 222 |
+
hv = np.zeros((nR, k), np.float32); np.add.at(hv, er_r, Vn[er_j] * er_w[:, None])
|
| 223 |
+
hn = np.linalg.norm(hv, axis=1); svcols[f'sv_k{k}_hcos'] = (hv * cand).sum(1) / (hn + 1e-9)
|
| 224 |
+
if k == 32:
|
| 225 |
+
svcols['sv_k32_hnorm'] = hn
|
| 226 |
+
sims = (Vn[er_j] * Vn[iic[er_r]]).sum(1)
|
| 227 |
+
mx = np.full(nR, -1.0, np.float32); np.maximum.at(mx, er_r, sims)
|
| 228 |
+
mx5 = np.full(nR, -1.0, np.float32); np.maximum.at(mx5, er_r[er_l5], sims[er_l5])
|
| 229 |
+
svcols['sv_k32_imax20'] = mx; svcols['sv_k32_imax5'] = mx5
|
| 230 |
+
if k == 64:
|
| 231 |
+
# click centroid vs non-clicked-impression centroid (impressions frozen at C: P_rows only)
|
| 232 |
+
Sc = sparse.csr_matrix((np.ones(cnt.height, np.float32), (cnt['u'].to_numpy(), cnt['ii'].to_numpy())), shape=(NU, NI))
|
| 233 |
+
neg = NEGsrc.filter(pl.col('clicked') == 0)
|
| 234 |
+
Sn = sparse.csr_matrix((np.ones(neg.height, np.float32), (neg['u'].to_numpy(), neg['ii'].to_numpy())), shape=(NU, NI))
|
| 235 |
+
nC = np.asarray(Sc @ Vn); nC /= np.linalg.norm(nC, axis=1, keepdims=True) + 1e-9
|
| 236 |
+
nN = np.asarray(Sn @ Vn); nN /= np.linalg.norm(nN, axis=1, keepdims=True) + 1e-9
|
| 237 |
+
cc = (nC[uu] * cand).sum(1); cn = (nN[uu] * cand).sum(1)
|
| 238 |
+
svcols['sv_k64_ccos'] = cc; svcols['sv_k64_ncos'] = cn
|
| 239 |
+
for a, nm in ((0.25, '025'), (0.5, '05'), (1.0, '1')):
|
| 240 |
+
M = nC - a * nN; M /= np.linalg.norm(M, axis=1, keepdims=True) + 1e-9
|
| 241 |
+
svcols[f'sv_k64_con{nm}'] = (M[uu] * cand).sum(1)
|
| 242 |
+
R = R.with_columns([pl.Series(c, v.astype(np.float32)) for c, v in svcols.items()])
|
| 243 |
+
|
| 244 |
+
# ---------------- MF ----------------
|
| 245 |
+
if with_mf:
|
| 246 |
+
mf = fit_mf(P_rows, NU, NI)
|
| 247 |
+
uu = R['u'].to_numpy().astype(np.int64); ii = R['ii'].to_numpy().astype(np.int64)
|
| 248 |
+
dot = (mf['P'][uu] * mf['Q'][ii]).sum(1)
|
| 249 |
+
R = R.with_columns(pl.Series('mf_score', mf['b'] + mf['bu'][uu] + mf['bi'][ii] + dot),
|
| 250 |
+
pl.Series('mf_dot', dot), pl.Series('mf_bi', mf['bi'][ii]))
|
| 251 |
+
|
| 252 |
+
fill0 = [c for c in R.columns if c.startswith(('it_imp', 'it_clk', 'it_n', 'us_n', 'hist_n', 'hist_imp', 'hist_noclk', 'co_')) and R[c].dtype != pl.List]
|
| 253 |
+
R = R.with_columns([pl.col(c).fill_null(0) for c in fill0])
|
| 254 |
+
R = R.with_columns([pl.col(c).fill_null(g) for c in ('it_ctr', 'it_ctr24', 'it_ctr6')] +
|
| 255 |
+
[pl.col(c).fill_null(1.0) for c in ('it_trend24', 'it_trend6', 'it_imptrend24')] +
|
| 256 |
+
[pl.col('us_ctr').fill_null(g), pl.col('it_imp_rate').fill_null(0), pl.col('us_repeat_rate').fill_null(0)])
|
| 257 |
+
|
| 258 |
+
# ---------------- event context ----------------
|
| 259 |
+
main = ['it_ctr', 'it_ctr24', 'it_imp', 'it_nclk_all', 'it_clk_dec2d', 'hist_n_all', 'hist_noclk',
|
| 260 |
+
'co_cos_max20', 'co_cos_rw', 'co_tr_max5', 'co_tr_last',
|
| 261 |
+
'sv_k16_ucos', 'sv_k32_ucos', 'sv_k64_ucos', 'sv_k32_hcos', 'sv_k64_hcos', 'sv_k32_imax20', 'sv_k64_con05', 'sv_k64_con025', 'sv_k64_ccos'] + (['mf_score', 'mf_dot'] if with_mf else [])
|
| 262 |
+
exprs = []
|
| 263 |
+
for c in main:
|
| 264 |
+
x = pl.col(c).cast(pl.Float64)
|
| 265 |
+
exprs += [x.rank('average').over('eid').alias(f'ev_{c}_rk'),
|
| 266 |
+
((x - x.mean().over('eid')) / (x.std().over('eid') + 1e-9)).alias(f'ev_{c}_z'),
|
| 267 |
+
x.max().over('eid').alias(f'ev_{c}_max'), x.mean().over('eid').alias(f'ev_{c}_mean')]
|
| 268 |
+
R = R.with_columns(exprs)
|
| 269 |
+
R = R.drop([c for c in R.columns if c.startswith('_')] + ['h', 'h_ts', 'tb', 'dt']).sort('r').drop('r')
|
| 270 |
+
log(f'build C={C} rows={R.height} cols={R.width} {time.time()-t0:.0f}s')
|
| 271 |
+
return R
|
| 272 |
+
|
| 273 |
+
GROUPS = ['it', 'hist', 'us', 'co', 'ev', 'tm', 'sv', 'mf']
|
| 274 |
+
|
| 275 |
+
def feature_cols(df, groups):
|
| 276 |
+
cols = []
|
| 277 |
+
for c in df.columns:
|
| 278 |
+
if c in ID_COLS: continue
|
| 279 |
+
gname = c.split('_')[0]
|
| 280 |
+
if gname == 'ev':
|
| 281 |
+
src = c[3:].split('_')[0] # ev_<src group>_...
|
| 282 |
+
if 'ev' in groups and src in groups: cols.append(c)
|
| 283 |
+
elif gname in groups:
|
| 284 |
+
cols.append(c)
|
| 285 |
+
return cols
|
parallel/src_snapshot/v3/leakcheck.py
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Leakage checks for the sv (SVD item-embedding / impression-contrast) features.
|
| 2 |
+
1) asserts: every history click feeding any click log has ts < C (train folds, val, eval)
|
| 3 |
+
2) rebuild f1..f4 + val25 with sv matrix variants and retrain at cutoff 25:
|
| 4 |
+
notgt = drop target events' own histories from the matrix; lag1d = matrix from clicks/impressions ts < C-1d
|
| 5 |
+
Usage: python leakcheck.py [notgt|lag1d ...]"""
|
| 6 |
+
import os, sys, json, time
|
| 7 |
+
import numpy as np, polars as pl
|
| 8 |
+
import train as T
|
| 9 |
+
from features import load, build, log, feature_cols, GROUPS, _hist_long
|
| 10 |
+
from split import J20, DAY, first_events
|
| 11 |
+
D = os.path.dirname(os.path.abspath(__file__))
|
| 12 |
+
TE, TR, EE, ER, NI, NU = load(f'{D}/data')
|
| 13 |
+
res = {}
|
| 14 |
+
# --- raw checks: history timestamps vs own event time and vs cutoff
|
| 15 |
+
for nm, E in (('train', TE), ('eval', EE)):
|
| 16 |
+
x = E.select('eid', 'ts', 'h_ts').explode('h_ts').drop_nulls('h_ts')
|
| 17 |
+
res[f'{nm}_hist_after_event'] = int((x['h_ts'] >= x['ts']).sum())
|
| 18 |
+
for C, nm in ((J20 + 5 * DAY, 'val25'), (J20 + 6 * DAY, 'val26')):
|
| 19 |
+
T_ev = first_events(TE, C)
|
| 20 |
+
h = _hist_long(T_ev)
|
| 21 |
+
res[f'{nm}_target_hist_clicks_ge_C'] = int((h['ts'] >= C).sum())
|
| 22 |
+
res[f'{nm}_target_min_ts_ge_C'] = bool(T_ev['ts'].min() >= C)
|
| 23 |
+
res['eval_min_event_ts_ge_Jan27'] = bool(EE['ts'].min() >= J20 + 7 * DAY)
|
| 24 |
+
res['train_max_event_ts_lt_Jan27'] = bool(TE['ts'].max() < J20 + 7 * DAY)
|
| 25 |
+
res['eval_hist_clicks_ge_Jan27'] = int((_hist_long(EE)['ts'] >= J20 + 7 * DAY).sum())
|
| 26 |
+
print(json.dumps(res, indent=1), flush=True)
|
| 27 |
+
feats = None
|
| 28 |
+
for mode in (sys.argv[1:] or ['notgt', 'lag1d']):
|
| 29 |
+
od = f'{D}/feat_leak/{mode}'; os.makedirs(od, exist_ok=True)
|
| 30 |
+
t0 = time.time()
|
| 31 |
+
for name, C, W in [(f'f{k}', J20 + k * DAY, DAY) for k in range(1, 5)] + [('val25', J20 + 5 * DAY, None)]:
|
| 32 |
+
out = f'{od}/{name}.parquet'
|
| 33 |
+
if os.path.exists(out): continue
|
| 34 |
+
T_ev = first_events(TE, C, W)
|
| 35 |
+
build(C, T_ev, TR.join(T_ev.select('eid'), on='eid', how='semi'), TE, TR, NI, NU, sv_mode=mode).write_parquet(out)
|
| 36 |
+
tr = pl.concat([pl.read_parquet(f'{od}/f{k}.parquet').with_columns(pl.lit(k, pl.Int16).alias('fold')) for k in range(1, 5)], how='diagonal_relaxed').sort('fold', 'eid')
|
| 37 |
+
va = pl.read_parquet(f'{od}/val25.parquet').with_columns(pl.lit(99, pl.Int16).alias('fold'))
|
| 38 |
+
fc = feature_cols(tr, GROUPS)
|
| 39 |
+
m, best, auc = T.fit(tr, va, fc, 'binary', 0.08, int(os.environ.get('THREADS', 4)))
|
| 40 |
+
svf = [c for c in fc if c.startswith('sv_')]
|
| 41 |
+
from metric import global_auc
|
| 42 |
+
single = {c: round(global_auc(va['clicked'].to_numpy(), va[c].fill_null(-9).to_numpy()), 4) for c in ('sv_k64_hcos', 'sv_k64_con05', 'sv_k32_ucos')}
|
| 43 |
+
r = dict(mode=mode, val25_auc=auc, best_iter=best, secs=round(time.time() - t0), single=single)
|
| 44 |
+
print(json.dumps(r), flush=True)
|
| 45 |
+
with open(f'{D}/out/leakcheck.jsonl', 'a') as f: f.write(json.dumps(r) + '\n')
|
parallel/src_snapshot/v3/metric.py
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Global ROC AUC over all rows (the competition metric; not per-event averaged)."""
|
| 2 |
+
import numpy as np
|
| 3 |
+
from sklearn.metrics import roc_auc_score
|
| 4 |
+
|
| 5 |
+
def global_auc(y, p):
|
| 6 |
+
y = np.asarray(y); p = np.asarray(p, dtype=np.float64)
|
| 7 |
+
if y.min() == y.max():
|
| 8 |
+
return float('nan')
|
| 9 |
+
return float(roc_auc_score(y, p))
|
parallel/src_snapshot/v3/predict.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Final fit + prediction.
|
| 2 |
+
1. (re)build features for folds f1..f6 (cutoffs Jan21..Jan26) and eval (cutoff Jan27) if missing (--rebuild forces).
|
| 3 |
+
2. train binary LightGBM on f1..f6 for n_iter = best_iter(cutoff 26 val) * scale (more data -> a few more rounds).
|
| 4 |
+
3. predict eval, write submission (clip [0,1], NaN->0.0629, %.8g), order = example_submission.csv.
|
| 5 |
+
Config read from out/best_config.json (written after validation); defaults below if missing.
|
| 6 |
+
Usage: python predict.py [--rebuild] [--out /workspace/huawei/submission_v2.csv]
|
| 7 |
+
"""
|
| 8 |
+
import argparse, json, os, subprocess, sys, time
|
| 9 |
+
import numpy as np, polars as pl
|
| 10 |
+
from train import load_table, fit, FOLDS, importance
|
| 11 |
+
from features import feature_cols, GROUPS
|
| 12 |
+
D = os.path.dirname(os.path.abspath(__file__))
|
| 13 |
+
|
| 14 |
+
def write_submission(rowid, pred, path, example='/workspace/huawei/example_submission.csv'):
|
| 15 |
+
p = np.nan_to_num(np.asarray(pred, dtype=np.float64), nan=0.0629, posinf=1.0, neginf=0.0).clip(0, 1)
|
| 16 |
+
df = pl.DataFrame({'rowid': rowid, 'predicted_probability': p})
|
| 17 |
+
ex = pl.read_csv(example, schema_overrides={'rowid': pl.Utf8}).select('rowid')
|
| 18 |
+
df = ex.join(df, on='rowid', how='left', maintain_order='left').with_columns(pl.col('predicted_probability').fill_null(0.0629))
|
| 19 |
+
with open(path, 'w', encoding='utf-8', newline='') as f:
|
| 20 |
+
f.write('rowid,predicted_probability\n')
|
| 21 |
+
f.writelines(f'{r},{v:.8g}\n' for r, v in zip(df['rowid'].to_list(), df['predicted_probability'].to_list()))
|
| 22 |
+
return df
|
| 23 |
+
|
| 24 |
+
if __name__ == '__main__':
|
| 25 |
+
ap = argparse.ArgumentParser()
|
| 26 |
+
ap.add_argument('--rebuild', action='store_true')
|
| 27 |
+
ap.add_argument('--out', default='/workspace/huawei/submission_v2.csv')
|
| 28 |
+
ap.add_argument('--threads', type=int, default=6)
|
| 29 |
+
a = ap.parse_args()
|
| 30 |
+
t0 = time.time()
|
| 31 |
+
cmd = [sys.executable, f'{D}/build_feats.py', 'all'] + (['--force'] if a.rebuild else [])
|
| 32 |
+
subprocess.run(cmd, check=True)
|
| 33 |
+
t_feat = time.time() - t0
|
| 34 |
+
cfg_path = f'{D}/out/best_config.json'
|
| 35 |
+
cfg = json.load(open(cfg_path)) if os.path.exists(cfg_path) else {}
|
| 36 |
+
groups = cfg.get('groups', GROUPS); drop = cfg.get('drop', [])
|
| 37 |
+
lr = cfg.get('lr', 0.05); n_iter = int(round(cfg.get('best_iter', 400) * cfg.get('scale', 1.15)))
|
| 38 |
+
params = cfg.get('params', None); folds = cfg.get('folds', FOLDS[27])
|
| 39 |
+
tr = load_table(folds)
|
| 40 |
+
feats = [f for f in feature_cols(tr, groups) if not any(f.startswith(d) for d in drop)]
|
| 41 |
+
print(f'final fit: {len(tr)} rows, {len(feats)} feats, lr={lr}, n_iter={n_iter}', flush=True)
|
| 42 |
+
t1 = time.time()
|
| 43 |
+
m, _, _ = fit(tr, None, feats, 'binary', lr, a.threads, max_rounds=n_iter, params=params)
|
| 44 |
+
t_fit = time.time() - t1
|
| 45 |
+
m.save_model(f'{D}/out/final_model.txt')
|
| 46 |
+
ev = pl.read_parquet(f'{D}/feat/eval.parquet')
|
| 47 |
+
pred = m.predict(ev.select(feats).to_numpy().astype(np.float32))
|
| 48 |
+
write_submission(ev['rowid'], pred, a.out)
|
| 49 |
+
info = dict(feature_build_secs=round(t_feat), fit_secs=round(t_fit), total_secs=round(time.time() - t0), n_iter=n_iter,
|
| 50 |
+
n_train_rows=len(tr), n_feats=len(feats), top15=importance(m))
|
| 51 |
+
json.dump(info, open(f'{D}/out/final_info.json', 'w'), indent=1)
|
| 52 |
+
print(json.dumps(info), flush=True)
|
| 53 |
+
subprocess.run([sys.executable, f'{D}/validate_submission.py', a.out], check=True)
|
parallel/src_snapshot/v3/prep.py
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Parse train/evaluation CSVs into compact parquet tables (dense int ids).
|
| 2 |
+
|
| 3 |
+
Outputs in v3/data/:
|
| 4 |
+
rows_train.parquet : rowid, eid, ii (item idx), clicked
|
| 5 |
+
rows_eval.parquet : rowid, eid, ii, ord (original row order)
|
| 6 |
+
ev_train.parquet / ev_eval.parquet : eid, u (user idx), ts, tb, dt, h (list[int item idx]), h_ts (list[int])
|
| 7 |
+
items.parquet / users.parquet : id string -> idx
|
| 8 |
+
Usage: python prep.py [DATA_DIR]
|
| 9 |
+
"""
|
| 10 |
+
import polars as pl, sys, time, os
|
| 11 |
+
DATA = sys.argv[1] if len(sys.argv) > 1 else '/workspace/huawei/data'
|
| 12 |
+
OUT = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'data')
|
| 13 |
+
os.makedirs(OUT, exist_ok=True)
|
| 14 |
+
t0 = time.time()
|
| 15 |
+
def log(*a): print(f'[{time.time()-t0:6.1f}s]', *a, flush=True)
|
| 16 |
+
|
| 17 |
+
def scan(name):
|
| 18 |
+
return pl.scan_csv(f'{DATA}/{name}.csv', schema_overrides={'history_item_ids': pl.Utf8, 'history_timestamps': pl.Utf8,
|
| 19 |
+
'rowid': pl.Utf8, 'event_id': pl.Utf8, 'user_id': pl.Utf8, 'item_id': pl.Utf8})
|
| 20 |
+
|
| 21 |
+
raw = {}
|
| 22 |
+
for name, lab in (('train', True), ('evaluation', False)):
|
| 23 |
+
lf = scan(name)
|
| 24 |
+
cols = ['rowid', 'event_id', 'item_id'] + (['clicked'] if lab else [])
|
| 25 |
+
rows = lf.select(cols).collect(engine='streaming')
|
| 26 |
+
ev = (lf.select('event_id', 'user_id', 'timestamp', 'time_bucket', 'day_type', 'history_item_ids', 'history_timestamps')
|
| 27 |
+
.unique(subset='event_id', keep='any')
|
| 28 |
+
.with_columns(pl.col('history_item_ids').str.json_decode(pl.List(pl.Utf8)).alias('h_str'),
|
| 29 |
+
pl.col('history_timestamps').str.json_decode(pl.List(pl.Int64)).alias('h_ts'))
|
| 30 |
+
.drop('history_item_ids', 'history_timestamps').collect(engine='streaming'))
|
| 31 |
+
raw[name] = (rows, ev)
|
| 32 |
+
log(name, rows.shape, ev.shape)
|
| 33 |
+
|
| 34 |
+
tr_rows, tr_ev = raw['train']; ev_rows, ev_ev = raw['evaluation']
|
| 35 |
+
items = pl.concat([tr_rows['item_id'], ev_rows['item_id'], tr_ev['h_str'].explode().drop_nulls(),
|
| 36 |
+
ev_ev['h_str'].explode().drop_nulls()]).unique().sort()
|
| 37 |
+
items = pl.DataFrame({'item_id': items, 'ii': pl.arange(0, len(items), eager=True).cast(pl.Int32)})
|
| 38 |
+
users = pl.concat([tr_ev['user_id'], ev_ev['user_id']]).unique().sort()
|
| 39 |
+
users = pl.DataFrame({'user_id': users, 'u': pl.arange(0, len(users), eager=True).cast(pl.Int32)})
|
| 40 |
+
events = pl.concat([tr_ev['event_id'], ev_ev['event_id']]).unique().sort()
|
| 41 |
+
events = pl.DataFrame({'event_id': events, 'eid': pl.arange(0, len(events), eager=True).cast(pl.Int32)})
|
| 42 |
+
log('items', len(items), 'users', len(users), 'events', len(events))
|
| 43 |
+
items.write_parquet(f'{OUT}/items.parquet'); users.write_parquet(f'{OUT}/users.parquet')
|
| 44 |
+
|
| 45 |
+
def map_rows(rows, lab):
|
| 46 |
+
r = rows.with_row_index('ord').join(events, on='event_id', how='left').join(items, on='item_id', how='left')
|
| 47 |
+
sel = ['ord', 'rowid', 'eid', 'ii'] + (['clicked'] if lab else [])
|
| 48 |
+
r = r.sort('ord').select(sel)
|
| 49 |
+
if lab: r = r.with_columns(pl.col('clicked').cast(pl.Int8))
|
| 50 |
+
return r
|
| 51 |
+
|
| 52 |
+
def map_ev(ev):
|
| 53 |
+
e = ev.join(events, on='event_id', how='left').join(users, on='user_id', how='left')
|
| 54 |
+
ex = e.select('eid', 'h_str').with_columns(pl.int_ranges(0, pl.col('h_str').list.len()).alias('k')).explode(['h_str', 'k']).drop_nulls('h_str')
|
| 55 |
+
ex = ex.join(items.rename({'item_id': 'h_str'}), on='h_str', how='left').sort('eid', 'k')
|
| 56 |
+
h = ex.group_by('eid', maintain_order=True).agg(pl.col('ii').alias('h'))
|
| 57 |
+
e = e.join(h, on='eid', how='left').with_columns(pl.col('h').fill_null(pl.lit([], dtype=pl.List(pl.Int32))))
|
| 58 |
+
return e.select('eid', 'u', pl.col('timestamp').cast(pl.Int64).alias('ts'), pl.col('time_bucket').cast(pl.Int8).alias('tb'),
|
| 59 |
+
pl.col('day_type').cast(pl.Int8).alias('dt'), 'h', 'h_ts').sort('ts', 'eid')
|
| 60 |
+
|
| 61 |
+
map_rows(tr_rows, True).drop('ord').write_parquet(f'{OUT}/rows_train.parquet')
|
| 62 |
+
map_rows(ev_rows, False).write_parquet(f'{OUT}/rows_eval.parquet')
|
| 63 |
+
map_ev(tr_ev).write_parquet(f'{OUT}/ev_train.parquet')
|
| 64 |
+
map_ev(ev_ev).write_parquet(f'{OUT}/ev_eval.parquet')
|
| 65 |
+
log('done')
|
parallel/src_snapshot/v3/run_all.sh
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env bash
|
| 2 |
+
# One-command rerun from the CSVs: parse -> features -> (optional validation) -> final fit -> submission -> validate.
|
| 3 |
+
# Usage: ./run_all.sh (final submission only, uses out/best_config.json)
|
| 4 |
+
# ./run_all.sh --validate (also baselines, ablation, val runs at cutoffs Jan25/Jan26)
|
| 5 |
+
set -euo pipefail
|
| 6 |
+
cd "$(dirname "$0")"
|
| 7 |
+
PY=/workspace/venv/bin/python
|
| 8 |
+
export POLARS_MAX_THREADS=${POLARS_MAX_THREADS:-6}
|
| 9 |
+
mkdir -p data feat logs out
|
| 10 |
+
$PY prep.py /workspace/huawei/data | tee logs/prep.log
|
| 11 |
+
$PY build_feats.py all --force | tee logs/build_feats.log
|
| 12 |
+
if [[ "${1:-}" == "--validate" ]]; then
|
| 13 |
+
$PY baselines.py | tee logs/baselines.log
|
| 14 |
+
$PY ablation.py | tee logs/ablation.log
|
| 15 |
+
$PY train.py --cutoff 25 --save full25 | tee logs/val25.log
|
| 16 |
+
$PY train.py --cutoff 26 --save full26 | tee logs/val26.log
|
| 17 |
+
$PY train.py --cutoff 25 --obj lambdarank | tee logs/lambdarank25.log
|
| 18 |
+
$PY segments.py full25 25 | tee logs/seg25.log; $PY segments.py full26 26 | tee logs/seg26.log
|
| 19 |
+
$PY leakcheck.py notgt lag1d | tee logs/leakcheck.log
|
| 20 |
+
fi
|
| 21 |
+
$PY predict.py --out /workspace/huawei/submission_v2.csv | tee logs/predict.log
|
parallel/src_snapshot/v3/segments.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Validation AUC by user segment. Usage: python segments.py full25 25"""
|
| 2 |
+
import sys, os, json
|
| 3 |
+
import numpy as np, polars as pl
|
| 4 |
+
from train import load_table
|
| 5 |
+
from metric import global_auc
|
| 6 |
+
D = os.path.dirname(os.path.abspath(__file__))
|
| 7 |
+
name, cut = sys.argv[1], int(sys.argv[2])
|
| 8 |
+
va = load_table([f'val{cut}']); p = np.load(f'{D}/out/{name}_valpred.npy'); y = va['clicked'].to_numpy()
|
| 9 |
+
cold = va['us_cold'].to_numpy() == 1; noh = va['us_nohist'].to_numpy() == 1
|
| 10 |
+
segs = {'all': np.ones(len(y), bool), 'users_with_fit_rows': ~cold, 'history_only (no fit rows)': cold & ~noh,
|
| 11 |
+
'empty_history': noh, 'cand_in_history': va['hist_in'].to_numpy() == 1, 'cand_not_in_history': va['hist_in'].to_numpy() == 0}
|
| 12 |
+
out = {}
|
| 13 |
+
for k, m in segs.items():
|
| 14 |
+
out[k] = dict(rows=int(m.sum()), events=int(m.sum() // 10), ctr=float(y[m].mean()) if m.any() else None,
|
| 15 |
+
auc=global_auc(y[m], p[m]) if m.any() else None)
|
| 16 |
+
print(f'{k:28s} rows={out[k]["rows"]:7d} ctr={out[k]["ctr"]:.4f} auc={out[k]["auc"]:.4f}')
|
| 17 |
+
json.dump(out, open(f'{D}/out/segments_{name}.json', 'w'), indent=1)
|