yeepee-ctr / parallel /report.md
Eric-Yyvuw's picture
sync 2026-10-23 13:31 IST | best val AUC 0.777500
1f78dbc verified
|
Raw History Blame Contribute Delete
9.8 kB

v2work vs v3: leakage audit and like-for-like comparison (parallel check)

Started 23 Oct 2026 12:35 (Europe/Dublin). v2work and v3 were only read. Everything here lives in /workspace/huawei/parallel/. The source snapshot comes from v2work/out/solution_v3_src.zip (06:24) and is unzipped in parallel/src_snapshot/. 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.

Status log

  • 12:45 Audit of the code path done (see below). Like-for-like scoring done (score_val.json). Submission check done (subcheck.json).

1. Leakage audit of the v2work "ne" (no-eval) path

Citations point to parallel/src_snapshot/v2work/* (identical to what produced out/submission_v3.csv). C is the cutoff. "Own history" is the history list that is a given input field on every row, including eval rows.

a. Are training rows only events before the cutoff? YES.

  • v3fold.py L11–12 SPEC: f_k = first event per user in [J20+kΒ·d, +1d).
  • In val mode, combo.py L44 trains on f1..f4 only (cut25). All target ts < Jan25.
  • Val25 is the first event per user at or after Jan25 (W=None), the same as our v3/split.
  • Final (combo.py L68, FOLDS unset in solution_v3.py) uses f1..f6, whose targets fall in Jan21..Jan26 (<Jan27).
  • build_folds.py L9–12: val25 is (Jan25, 2d), and h1..h4 are 1-day windows.

b. Are label-derived features computed only from labels before the cutoff? YES.

  • v3feat_ne.py L109–110: P_ev and P_rows are filtered to ts<C. All CTR, impression and click stats come from these.
  • feats.py L40–41: same filter.
  • extra.py L22–24 and extra2.py L29–31 assert CL.ts.max()<C and P_rows.ts.max()<C.
  • Cluster CTR (extra.py L60) and user state (extra2.py L53–59) come from P_rows and P_ev.

c. Does any feature use the unlabelled eval rows? Only their own history, and only for user-level features.

  • Item, global, co-occurrence and SVD stats come from CLg = click_log(..., T_ev.head(0)), which is train only:
    • v3feat_ne.py L113–114 (CL vs CLg)
    • L128 (item ip)
    • L173 (cooc)
    • L195 (SVD from CLg)
    • feats.py L54–55 poolg β†’ L65, L87, L96
    • extra.py L31 (idf from CLg)
    • extra2.py L35–39 (SVD)
  • User-level stats use each row's own pre-C history:
    • v3feat_ne.py L209–212 and L222 (user fold-in onto train-only item factors)
    • feats.py L49–50 and L117–122
    • extra.py L36
    • extra2.py L41
  • This is standard: every eval row carries its own history. It doesn't pool across eval rows. No eval labels exist or are used.
  • Two minor transductive touches, label-free with pre-C clicks only:
    • 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.
    • extra.py L55–57 fits KMeans on Un over active users, which can include target users.
    • Neither uses labels or post-C data. Expected effect is small.
  • write_sub.py reads evaluation.csv only for rowid order and validation.

d. Is the validation set the same as ours? YES.

  • val25: 439,640 rows, rowid sets identical, 0 label mismatches.
  • val26: 351,380 rows, identical (score_val.json).

Does submission_v3.csv come from the ne path? YES.

  • 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.
  • 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).
  • 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).

FLAG: our own v3 is less strict than v2work here.

  • 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.
  • For eval predictions this means our submission_v2.csv pools all eval rows' (pre-cutoff, label-free) histories into item-level stats.
  • That's the same pattern Grok Bot labelled a leak and superseded in its old v2.
  • 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.

2. Like-for-like scoring (our metric: global ROC AUC, v4/common.report), identical rows

v3 preds come from re-predicting the saved models v3/out/full25.txt and full26_lr05.txt in parquet row order. 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.

set model AUC eval-weighted stale ≀1d stale 1–2d event AUC* Spearman vs v3
val25 (439,640) v3 (ours) 0.75554 0.75554 0.76059 0.73424 0.64894 –
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
val25 v2work notime_x2 (pre-ne config, 04:26) 0.77536 0.77535 0.77968 0.75708 0.65328 0.899
val25 v2work screen, base, 25%-user train, 1 seed 0.77174 0.77173 0.77642 0.75176 0.59153* 0.888
val26 (351,380) v3 (ours) 0.77444 0.77444 0.77444 – 0.65666 –
val26 v2work screen, base, 25%-user train 0.78921 0.78921 0.78921 – 0.59846* 0.912

*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.

Notes:

  • 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.
  • There are no full-data v2work val26 preds. The 25% screen alone beats our full v3 by +0.015.
  • 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.

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.

3. Reproduction and lag-1d leak test (parallel/lag/, one job at a time, 3 threads, seed 42, deterministic)

Reproduction.

  • 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.
  • 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.
  • I also rebuilt the val25 tables from the snapshot code (lag/repro_cmp2.json):
    • feat_x2 is exact.
    • feat (v2) has 31 of 122 cols with tiny diffs (corr β‰₯0.9997).
    • feat_v3 has 60 of 169 co-occurrence/rank cols differing on ≀2.5% of rows (corr β‰₯0.989).
    • feat_x has clctr cols differing on ≀5% of rows (corr β‰₯0.996).
    • These diffs are consistent with thread-dependent randomized SVD/KMeans and tie-breaking. They don't point to a different (eval-pooled) build.

Lag-1d test.

  • 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.
  • Predicted with the same model (lag/lagtest.py):
val25 features AUC stale ≀1d stale 1–2d event AUC
as built (C=Jan25) 0.77748 0.78184 0.75895 0.65358
lag-1d (C=Jan24) 0.76470 0.76946 0.74430 0.64837
  • 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).
  • Even with 1-day-stale features, v2work (0.7647) beats our v3 with fresh features (0.7555).
  • A target-day leak would collapse towards ~0.65–0.70 when the target day is removed. That doesn't happen.

4. Verdict

v2work's ne pipeline is genuinely better than our v3.

  • val25: +0.022 AUC (0.7775 vs 0.7555) on identical rows and labels.
  • val26 screen: +0.015, even with a 25% train sample.
  • The gain holds in every staleness bucket and survives the lag-1d test.

The audit found no label leakage:

  • Training targets are before C.
  • All label stats come from ts<C, with asserts.
  • Item and global stats are train only.
  • Eval rows contribute only their own given pre-cutoff history to user-level features.

Grok Bot's claims all check out:

  • The ne variant files.
  • 0.7775 from ne_combo25 and ne_combo25_eft.
  • submission_v3.csv built by the 06:30 combo final run (it equals eval_pred_v3.parquet).
  • The old v2work submission_v2 is superseded.

Correlation with our submission: Spearman 0.918.

Flags for Eric:

  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.
  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.
  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.

Final CSV: it exists. v2work/out/submission_v3.csv: sha256 466f3d64…, 529,390 rows, written 06:37, plus solution_v3.py and src zip.

  • 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).
  • If all tables are reused, only the final fit is needed: ~15–20 min. I didn't build one.