|
| 1 | +#!/usr/bin/env python3 |
| 2 | +"""V114 REPRESENTATION -> APPLICABILITY intervention. |
| 3 | +
|
| 4 | +Question left by V112/V113: |
| 5 | + V112: raw transcript views did not improve direct label prediction. |
| 6 | + V113: geometry/support/session metadata did not recover the endpoint-oracle gap. |
| 7 | +
|
| 8 | +V114 asks the missing cross: can richer row-level representation predict WHICH already-capable |
| 9 | +endpoint (V75 or RELATED) should apply? This is an applicability target, not another label model. |
| 10 | +
|
| 11 | +Frozen protocol: |
| 12 | +- deterministic 2500-row sample (same hash rule as V112/V113) |
| 13 | +- objective-grouped 4-fold outer OOF |
| 14 | +- endpoints trained only on outer-train rows |
| 15 | +- oracle-choice target formed per row from endpoint losses, used only inside outer-train for gate fit |
| 16 | +- fixed conservative routing weight 0.65; no hyperparameter sweep |
| 17 | +- families: geometry, objective semantics, raw transcript, objective+raw, full representation |
| 18 | +- controls: response/session ID placebo; shuffled applicability target; flipped-route ablation |
| 19 | +
|
| 20 | +Decision thresholds (precommitted before result): |
| 21 | +- PHASE_CHANGE_REPRESENTATION: gain >= .010 and all folds nonnegative, OR gain >= .008 and >=15% oracle-gap recovery |
| 22 | +- REPRESENTATION_REPAIR_FOUND: gain >= .003, >=3/4 positive folds, controls <25% real gain, flipped route <=0 |
| 23 | +- STRUCTURED_REPRESENTATION_HINT: .001 <= gain < .003 and best family beats geometry by >=.001 |
| 24 | +- otherwise REPRESENTATION_NOT_OBSERVED |
| 25 | +""" |
| 26 | +from __future__ import annotations |
| 27 | +import argparse, hashlib, json |
| 28 | +from pathlib import Path |
| 29 | +import numpy as np |
| 30 | +from scipy.sparse import hstack, csr_matrix |
| 31 | +from sklearn.feature_extraction.text import HashingVectorizer |
| 32 | +from sklearn.linear_model import LogisticRegression |
| 33 | +from sklearn.model_selection import GroupKFold |
| 34 | +from sklearn.ensemble import HistGradientBoostingClassifier |
| 35 | +from v71_mastery_events import load_transcript, normalize_roles |
| 36 | +from v75_canonical_trajectory import load_training, SEED |
| 37 | +from v81_target_segment_phase import choose_target_segment |
| 38 | +from v85_evidence_state import build_v75 |
| 39 | +from v94_related_control import segmented_control, build_control |
| 40 | +from v110_residual_collider_state_discovery import hb, ll |
| 41 | + |
| 42 | +EPS=1e-5 |
| 43 | + |
| 44 | +def H(x): return int(hashlib.sha256(str(x).encode()).hexdigest()[:16],16) |
| 45 | +def lossrow(y,p): |
| 46 | + p=np.clip(p,EPS,1-EPS) |
| 47 | + return -(y*np.log(p)+(1-y)*np.log(1-p)) |
| 48 | +def geometry(p0,pr): |
| 49 | + d=pr-p0 |
| 50 | + return np.c_[p0,pr,d,np.abs(d),np.abs(p0-.5),np.abs(pr-.5),np.minimum(p0,pr),np.maximum(p0,pr)] |
| 51 | +def transcript_views(df,obj): |
| 52 | + d=normalize_roles(df).reset_index(drop=True) |
| 53 | + roles=d.role_repaired.astype(str).tolist(); c=d.content.fillna('').astype(str).tolist() |
| 54 | + stu=' '.join(x for r,x in zip(roles,c) if r=='student') |
| 55 | + tut=' '.join(x for r,x in zip(roles,c) if r=='tutor') |
| 56 | + full=' '.join(f'[{r}] {x}' for r,x in zip(roles,c)) |
| 57 | + seg,_=choose_target_segment(df,obj); s=normalize_roles(seg).reset_index(drop=True) |
| 58 | + local=' '.join(f'[{r}] {x}' for r,x in zip(s.role_repaired.astype(str),s.content.fillna('').astype(str))) |
| 59 | + last=' '.join(f'[{r}] {x}' for r,x in list(zip(roles,c))[-8:]) |
| 60 | + return stu,tut,full,local,last |
| 61 | + |
| 62 | +def route(p0,pr,g,flip=False): |
| 63 | + if flip: g=1-g |
| 64 | + w=np.clip(.65*g,0,.65) |
| 65 | + return np.clip((1-w)*p0+w*pr,EPS,1-EPS) |
| 66 | +def fit_dense_gate(X,win,sw,tr,va): |
| 67 | + m=HistGradientBoostingClassifier(max_depth=2,max_iter=70,learning_rate=.05,min_samples_leaf=80,l2_regularization=2.,random_state=SEED) |
| 68 | + m.fit(X[tr],win[tr],sample_weight=sw[tr]) |
| 69 | + return m.predict_proba(X[va])[:,1] |
| 70 | +def fit_sparse_gate(X,win,sw,tr,va,shuffle=False): |
| 71 | + yt=win[tr].copy() |
| 72 | + if shuffle: |
| 73 | + rng=np.random.default_rng(SEED+len(tr)+len(va)); yt=yt[rng.permutation(len(yt))] |
| 74 | + # geometry is already concatenated into X; fixed regularization, no sweep |
| 75 | + m=LogisticRegression(C=.08,max_iter=220,solver='liblinear',random_state=SEED) |
| 76 | + m.fit(X[tr],yt,sample_weight=sw[tr]) |
| 77 | + return m.predict_proba(X[va])[:,1] |
| 78 | +def main(a): |
| 79 | + f0=load_training(a.features,a.labels).reset_index(drop=True) |
| 80 | + print('features columns',list(f0.columns),flush=True) |
| 81 | + objall=(f0.learning_objective_id if 'learning_objective_id' in f0 else f0.learning_objective).astype(str).to_numpy() |
| 82 | + cand=np.where(np.array([hb(x,5)!=0 for x in objall]))[0] |
| 83 | + ix=np.array(sorted(cand,key=lambda i:H(f0.response_id.iloc[i]))[:a.rows]) |
| 84 | + f=f0.iloc[ix].reset_index(drop=True) |
| 85 | + y=f.target.to_numpy(int) |
| 86 | + obj=(f.learning_objective_id if 'learning_objective_id' in f else f.learning_objective).astype(str).to_numpy() |
| 87 | + key=f.learning_objective.astype(str).to_numpy(); sess=f.session_id.astype(str).to_numpy() |
| 88 | + cache={s:load_transcript(a.transcripts/f'{s}.csv') for s in np.unique(sess)} |
| 89 | + rt=[]; rz=[]; T={k:[] for k in ['STUDENT','TUTOR','FULL','LOCAL','LAST8']} |
| 90 | + for _,r in f.iterrows(): |
| 91 | + d=cache[str(r.session_id)] |
| 92 | + t,z=segmented_control(d,str(r.learning_objective),'related'); rt.append(t); rz.append(z) |
| 93 | + vals=transcript_views(d,str(r.learning_objective)) |
| 94 | + for k,v in zip(T,vals): T[k].append(v) |
| 95 | + X75=build_v75(f,cache); Xr=build_control(rt,rz) |
| 96 | + P0=np.zeros(len(f)); PR=np.zeros(len(f)); fold=np.full(len(f),-1,int) |
| 97 | + splits=list(GroupKFold(min(4,len(np.unique(obj)))).split(np.zeros(len(y)),y,obj)) |
| 98 | + for k,(tr,va) in enumerate(splits): |
| 99 | + m0=LogisticRegression(C=.25,max_iter=300,solver='liblinear',random_state=SEED).fit(X75[tr],y[tr]) |
| 100 | + mr=LogisticRegression(C=.25,max_iter=300,solver='liblinear',random_state=SEED).fit(Xr[tr],y[tr]) |
| 101 | + P0[va]=np.clip(m0.predict_proba(X75[va])[:,1],EPS,1-EPS) |
| 102 | + PR[va]=np.clip(mr.predict_proba(Xr[va])[:,1],EPS,1-EPS); fold[va]=k |
| 103 | + # exact-support V97 reconstruction, same rule as V113 |
| 104 | + base=np.zeros(len(y)) |
| 105 | + allidx=np.arange(len(y)) |
| 106 | + for i in range(len(y)): |
| 107 | + tr=allidx[fold!=fold[i]] |
| 108 | + base[i]=.65*P0[i]+.35*PR[i] if np.sum(key[tr]==key[i])==0 else P0[i] |
| 109 | + base=np.clip(base,EPS,1-EPS) |
| 110 | + base_ll=ll(y,base) |
| 111 | + L0=lossrow(y,P0); LR=lossrow(y,PR); win=(LR<L0).astype(int); sw=np.abs(L0-LR)+.01 |
| 112 | + oracle=np.where(win==1,PR,P0); oracle_ll=ll(y,oracle); gap=base_ll-oracle_ll |
| 113 | + G=geometry(P0,PR) |
| 114 | + hvw=HashingVectorizer(n_features=2**15,alternate_sign=False,norm='l2',ngram_range=(1,2),lowercase=True) |
| 115 | + hvc=HashingVectorizer(n_features=2**15,alternate_sign=False,norm='l2',analyzer='char_wb',ngram_range=(3,5),lowercase=True) |
| 116 | + OBJW=hvw.transform(key); OBJC=hvc.transform(key) |
| 117 | + RAW=hstack([hvw.transform(T['STUDENT']),hvw.transform(T['TUTOR']),hvw.transform(T['LOCAL']),hvw.transform(T['LAST8'])],format='csr') |
| 118 | + GS=csr_matrix(G) |
| 119 | + ID=csr_matrix(np.c_[np.array([H(x)%997 for x in f.response_id.astype(str)])/997.,np.array([H(x)%31 for x in sess])/31.]) |
| 120 | + mats={ |
| 121 | + 'GEOMETRY':G, |
| 122 | + 'OBJECTIVE_SEMANTICS':hstack([GS,OBJW,OBJC],format='csr'), |
| 123 | + 'RAW_TRANSCRIPT':hstack([GS,RAW],format='csr'), |
| 124 | + 'OBJECTIVE_X_RAW':hstack([GS,OBJW,OBJC,RAW],format='csr'), |
| 125 | + 'FULL_REPRESENTATION':hstack([GS,OBJW,OBJC,RAW,csr_matrix(X75),csr_matrix(Xr)],format='csr'), |
| 126 | + 'ID_PLACEBO':hstack([GS,ID],format='csr') |
| 127 | + } |
| 128 | + preds={k:np.zeros(len(y)) for k in mats}; shuffled=np.zeros(len(y)); gate_keep={k:np.zeros(len(y)) for k in mats} |
| 129 | + for _,(tr,va) in enumerate(splits): |
| 130 | + for name,X in mats.items(): |
| 131 | + gp=fit_dense_gate(X,win,sw,tr,va) if name=='GEOMETRY' else fit_sparse_gate(X,win,sw,tr,va) |
| 132 | + gate_keep[name][va]=gp; preds[name][va]=route(P0[va],PR[va],gp) |
| 133 | + shuffled[va]=route(P0[va],PR[va],fit_sparse_gate(mats['OBJECTIVE_X_RAW'],win,sw,tr,va,shuffle=True)) |
| 134 | + tests={} |
| 135 | + for name,q in preds.items(): |
| 136 | + fg=[float(ll(y[va],base[va])-ll(y[va],q[va])) for _,va in splits] |
| 137 | + tests[name]={'ll':float(ll(y,q)),'gain':float(base_ll-ll(y,q)),'fold_gains':fg,'positive_folds':int(np.sum(np.array(fg)>0))} |
| 138 | + shgain=float(base_ll-ll(y,shuffled)) |
| 139 | + real=['OBJECTIVE_SEMANTICS','RAW_TRANSCRIPT','OBJECTIVE_X_RAW','FULL_REPRESENTATION'] |
| 140 | + winner=max(real,key=lambda n:tests[n]['gain']); gain=tests[winner]['gain']; rec=gain/gap if gap>0 else 0. |
| 141 | + flipped=route(P0,PR,gate_keep[winner],flip=True); flipped_gain=float(base_ll-ll(y,flipped)) |
| 142 | + geometry_gain=tests['GEOMETRY']['gain']; idgain=tests['ID_PLACEBO']['gain']; control=max(idgain,shgain) |
| 143 | + phase=(gain>=.010 and min(tests[winner]['fold_gains'])>=0) or (gain>=.008 and rec>=.15) |
| 144 | + found=(gain>=.003 and tests[winner]['positive_folds']>=3 and control<.25*gain and flipped_gain<=0) |
| 145 | + hint=(.001<=gain<.003 and gain-geometry_gain>=.001) |
| 146 | + verdict='PHASE_CHANGE_REPRESENTATION' if phase else 'REPRESENTATION_REPAIR_FOUND' if found else 'STRUCTURED_REPRESENTATION_HINT' if hint else 'REPRESENTATION_NOT_OBSERVED' |
| 147 | + out={ |
| 148 | + 'rows':len(y),'objectives':len(np.unique(obj)),'v97':base_ll,'row_endpoint_oracle':oracle_ll,'oracle_gap':gap, |
| 149 | + 'oracle_related_win_rate':float(np.mean(win)),'tests':tests,'winner':winner,'winner_gain':gain, |
| 150 | + 'oracle_gap_recovered_fraction':rec,'controls':{'shuffled_applicability_gain':shgain,'id_placebo_gain':idgain,'flipped_winner_route_gain':flipped_gain}, |
| 151 | + 'representation_increment_over_geometry':float(gain-geometry_gain),'decision':verdict, |
| 152 | + 'precommit':{ |
| 153 | + 'phase':'gain >=.010 and all folds nonnegative OR gain >=.008 and >=15% oracle recovery', |
| 154 | + 'repair_found':'gain >=.003, >=3/4 positive folds, controls <25% real gain, flipped route <=0', |
| 155 | + 'structured':'.001-.003 and representation beats geometry by >=.001', |
| 156 | + 'otherwise':'representation not observed' |
| 157 | + } |
| 158 | + } |
| 159 | + Path(a.out).write_text(json.dumps(out,indent=2)); print(json.dumps(out,indent=2),flush=True) |
| 160 | +if __name__=='__main__': |
| 161 | + 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('--rows',type=int,default=2500); p.add_argument('--out',default='v114_representation_applicability.json'); main(p.parse_args()) |
0 commit comments