|
| 1 | +#!/usr/bin/env python3 |
| 2 | +"""V107: support-conditioned V74 prior on top of frozen V97. |
| 3 | +
|
| 4 | +Hypothesis from V106: V74 helps supported/seen-objective regimes but hurts exact-unseen |
| 5 | +objectives. Preserve V97 exactly on unseen objectives and add a small fixed V74 prior |
| 6 | +only when the exact objective has fold-local training support. |
| 7 | +
|
| 8 | +This is a fixed-law confirmation: no labels select weights. |
| 9 | +""" |
| 10 | +from __future__ import annotations |
| 11 | +import argparse, hashlib, json |
| 12 | +from pathlib import Path |
| 13 | +import numpy as np |
| 14 | +from sklearn.cluster import KMeans |
| 15 | +from sklearn.linear_model import LogisticRegression |
| 16 | +from sklearn.metrics import log_loss |
| 17 | + |
| 18 | +from v71_mastery_events import load_transcript |
| 19 | +from v74_semantic_objective_prior import semantic_prior_predict |
| 20 | +from v75_canonical_trajectory import load_training, SEED |
| 21 | +from v85_evidence_state import build_v75 |
| 22 | +from v93_shift_robust_validation import obj_family, style_matrix |
| 23 | +from v94_related_control import segmented_control, build_control |
| 24 | + |
| 25 | +EPS=1e-5 |
| 26 | +W_V97_UNSEEN=0.35 |
| 27 | +W74_SEEN=0.20 |
| 28 | + |
| 29 | + |
| 30 | +def ll(y,p): return float(log_loss(y,np.clip(p,EPS,1-EPS),labels=[0,1])) |
| 31 | +def fit_lr(X,y,tr,va): |
| 32 | + m=LogisticRegression(C=.25,max_iter=300,solver='liblinear',random_state=SEED).fit(X[tr],y[tr]) |
| 33 | + return np.clip(m.predict_proba(X[va])[:,1],EPS,1-EPS) |
| 34 | +def bucket(x,n=5): |
| 35 | + h=int(hashlib.sha256(str(x).encode()).hexdigest()[:16],16) |
| 36 | + return h % n |
| 37 | +def hash_folds(groups,n=5): |
| 38 | + g=np.asarray(groups).astype(str); idx=np.arange(len(g)); out=[] |
| 39 | + gb=np.asarray([bucket(x,n) for x in g]) |
| 40 | + for k in range(n): |
| 41 | + va=idx[gb==k]; tr=idx[gb!=k]; out.append((tr,va)) |
| 42 | + return out |
| 43 | +def mixed_support_folds(obj,sess,n=5): |
| 44 | + idx=np.arange(len(obj)); ob=np.asarray([bucket(x,n) for x in obj]); sb=np.asarray([bucket(x,n) for x in sess]); out=[] |
| 45 | + for k in range(n): |
| 46 | + cold=ob==k |
| 47 | + seen_session=(ob!=k)&(sb==k) |
| 48 | + va=idx[cold|seen_session]; tr=idx[~(cold|seen_session)]; out.append((tr,va)) |
| 49 | + return out |
| 50 | +def unseen_mask(keys,tr,va): |
| 51 | + seen=set(keys[tr].tolist()); return np.asarray([keys[i] not in seen for i in va],bool) |
| 52 | + |
| 53 | +def run(a): |
| 54 | + f=load_training(a.features,a.labels).reset_index(drop=True) |
| 55 | + cache={sid:load_transcript(a.transcripts/f'{sid}.csv') for sid in f.session_id.astype(str).unique()} |
| 56 | + rt=[]; rz=[] |
| 57 | + for i,r in f.iterrows(): |
| 58 | + t,z=segmented_control(cache[str(r.session_id)],str(r.learning_objective),'related'); rt.append(t); rz.append(z) |
| 59 | + if (i+1)%2500==0: print('rows',i+1,flush=True) |
| 60 | + X75=build_v75(f,cache); Xr=build_control(rt,rz); y=f.target.to_numpy(int) |
| 61 | + obj=(f.learning_objective_id if 'learning_objective_id' in f else f.learning_objective).astype(str).to_numpy() |
| 62 | + support=f.learning_objective.astype(str).to_numpy(); sess=f.session_id.astype(str).to_numpy() |
| 63 | + fam=f.learning_objective.astype(str).map(obj_family).astype(str).to_numpy() |
| 64 | + style=KMeans(n_clusters=5,random_state=137,n_init=10).fit(style_matrix(f,cache)).labels_.astype(str) |
| 65 | + worlds={ |
| 66 | + 'objective_cold':hash_folds(obj), |
| 67 | + 'session_cold':hash_folds(sess), |
| 68 | + 'objective_family_cold':hash_folds(fam), |
| 69 | + 'style_cold':hash_folds(style), |
| 70 | + 'mixed_support':mixed_support_folds(obj,sess), |
| 71 | + } |
| 72 | + out={'law':{'unseen':'V97 = .65 V75 + .35 RELATED','seen':'0.80 V75 + 0.20 V74'},'worlds':{}} |
| 73 | + gains=[] |
| 74 | + for name,sp in worlds.items(): |
| 75 | + p97=np.zeros(len(y)); p107=np.zeros(len(y)); U=np.zeros(len(y),bool); folds=[] |
| 76 | + for k,(tr,va) in enumerate(sp,1): |
| 77 | + p75=fit_lr(X75,y,tr,va); pr=fit_lr(Xr,y,tr,va); p74,_=semantic_prior_predict(f.iloc[tr],f.iloc[va]) |
| 78 | + uns=unseen_mask(support,tr,va); U[va]=uns |
| 79 | + q97=np.where(uns,.65*p75+.35*pr,p75) |
| 80 | + q107=np.where(uns,q97,.80*p75+.20*p74) |
| 81 | + q97=np.clip(q97,EPS,1-EPS); q107=np.clip(q107,EPS,1-EPS) |
| 82 | + p97[va]=q97; p107[va]=q107 |
| 83 | + folds.append({'fold':k,'rows':int(len(va)),'unseen_fraction':float(uns.mean()),'v97':ll(y[va],q97),'v107':ll(y[va],q107),'gain':ll(y[va],q97)-ll(y[va],q107)}) |
| 84 | + print(name,folds[-1],flush=True) |
| 85 | + rec={'v97':ll(y,p97),'v107':ll(y,p107),'gain_vs_v97':ll(y,p97)-ll(y,p107),'unseen_fraction':float(U.mean()),'folds':folds} |
| 86 | + out['worlds'][name]=rec; gains.append(rec['gain_vs_v97']); print(name,'SUMMARY',rec,flush=True) |
| 87 | + g=np.asarray(gains,float); mixed=out['worlds']['mixed_support']['gain_vs_v97']; session=out['worlds']['session_cold']['gain_vs_v97']; objg=out['worlds']['objective_cold']['gain_vs_v97'] |
| 88 | + promote=(g.mean()>=.0005 and mixed>=.0003 and session>=.0003 and objg>=-.0002 and g.min()>=-.0003) |
| 89 | + out['decision']={'mean_gain_vs_v97':float(g.mean()),'mixed_support_gain':float(mixed),'session_cold_gain':float(session),'objective_cold_gain':float(objg),'worst_gain':float(g.min()),'verdict':'PROMOTE_V107_RUNTIME' if promote else 'KEEP_V97','precommit':'promote iff mean >= .0005, mixed >= .0003, session >= .0003, objective >= -.0002, worst >= -.0003'} |
| 90 | + Path(a.out).write_text(json.dumps(out,indent=2)); print(json.dumps(out,indent=2),flush=True) |
| 91 | + |
| 92 | +if __name__=='__main__': |
| 93 | + p=argparse.ArgumentParser(); p.add_argument('--features',type=Path,required=True); p.add_argument('--labels',type=Path,required=True); p.add_argument('--transcripts',type=Path,required=True); p.add_argument('--out',default='v107_support_conditioned_prior.json'); run(p.parse_args()) |
0 commit comments