Eric-Yyvuw commited on
Commit
1f78dbc
·
verified ·
1 Parent(s): 738aefe

sync 2026-10-23 13:31 IST | best val AUC 0.777500

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. README.md +2 -2
  2. parallel/lag/b_v2.py +7 -0
  3. parallel/lag/b_v3.py +8 -0
  4. parallel/lag/build.log +9 -0
  5. parallel/lag/cmp.py +14 -0
  6. parallel/lag/cmp2.py +14 -0
  7. parallel/lag/extra.py +79 -0
  8. parallel/lag/extra2.py +74 -0
  9. parallel/lag/lag_buckets.json +1 -0
  10. parallel/lag/lagtest.json +13 -0
  11. parallel/lag/lagtest.py +37 -0
  12. parallel/lag/r_v2.py +7 -0
  13. parallel/lag/r_v3.py +8 -0
  14. parallel/lag/repro_cmp.json +173 -0
  15. parallel/lag/repro_cmp2.json +188 -0
  16. parallel/lag/run2.log +12 -0
  17. parallel/report.md +135 -0
  18. parallel/score.log +102 -0
  19. parallel/score.py +29 -0
  20. parallel/score_val.json +100 -0
  21. parallel/src_snapshot/v2work/blend_val.py +44 -0
  22. parallel/src_snapshot/v2work/build_folds.py +27 -0
  23. parallel/src_snapshot/v2work/combo.py +85 -0
  24. parallel/src_snapshot/v2work/emb_exp.py +38 -0
  25. parallel/src_snapshot/v2work/explore.py +26 -0
  26. parallel/src_snapshot/v2work/extra.py +78 -0
  27. parallel/src_snapshot/v2work/extra2.py +73 -0
  28. parallel/src_snapshot/v2work/feats.py +225 -0
  29. parallel/src_snapshot/v2work/feats_pre_noeval.py +215 -0
  30. parallel/src_snapshot/v2work/prep.py +22 -0
  31. parallel/src_snapshot/v2work/starter_split.py +12 -0
  32. parallel/src_snapshot/v2work/starter_val.py +29 -0
  33. parallel/src_snapshot/v2work/train_val.py +39 -0
  34. parallel/src_snapshot/v2work/tt.py +59 -0
  35. parallel/src_snapshot/v2work/tt_exp.py +15 -0
  36. parallel/src_snapshot/v2work/v3feat_ne.py +290 -0
  37. parallel/src_snapshot/v2work/v3fold.py +18 -0
  38. parallel/src_snapshot/v2work/validate.py +18 -0
  39. parallel/src_snapshot/v2work/write_sub.py +27 -0
  40. parallel/src_snapshot/v3/ablation.py +22 -0
  41. parallel/src_snapshot/v3/baselines.py +38 -0
  42. parallel/src_snapshot/v3/blend.py +16 -0
  43. parallel/src_snapshot/v3/build_feats.py +29 -0
  44. parallel/src_snapshot/v3/features.py +285 -0
  45. parallel/src_snapshot/v3/leakcheck.py +45 -0
  46. parallel/src_snapshot/v3/metric.py +9 -0
  47. parallel/src_snapshot/v3/predict.py +53 -0
  48. parallel/src_snapshot/v3/prep.py +65 -0
  49. parallel/src_snapshot/v3/run_all.sh +21 -0
  50. 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 12:26 IST · 165 files._
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,public_auc,,0.788757
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)