Skip to content

Commit 757bbd6

Browse files
committed
Add V114 representation applicability intervention
1 parent 5f5a629 commit 757bbd6

1 file changed

Lines changed: 161 additions & 0 deletions

File tree

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
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

Comments
 (0)