Skip to content

Commit 3afa96c

Browse files
committed
trace ace: add V104 single session separator
1 parent 1f00569 commit 3afa96c

1 file changed

Lines changed: 117 additions & 0 deletions

File tree

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
#!/usr/bin/env python3
2+
"""V104: single-separator test for session-level applicability.
3+
4+
V102 localized most oracle headroom to session resolution, while V103's multivariate
5+
session regressor overfit/regressed. This test asks the cheapest next question: does one
6+
runtime-visible session statistic lawfully separate sessions that want MORE vs LESS
7+
RELATED than frozen V97's 0.35 blend?
8+
9+
Selection is nested. Outer objective-cold folds are untouched. Inside each outer-train,
10+
objective-grouped OOF expert predictions are used to choose exactly one feature,
11+
threshold, direction, and two blend weights. The chosen rule is then applied unchanged
12+
to the outer validation fold.
13+
"""
14+
from __future__ import annotations
15+
import argparse, json
16+
from pathlib import Path
17+
import numpy as np
18+
from sklearn.linear_model import LogisticRegression
19+
from sklearn.metrics import log_loss
20+
from sklearn.model_selection import GroupKFold
21+
from v71_mastery_events import load_transcript, tokens
22+
from v75_canonical_trajectory import load_training, SEED
23+
from v85_evidence_state import build_v75
24+
from v93_shift_robust_validation import folds_from_groups
25+
from v94_related_control import segmented_control, build_control
26+
27+
EPS=1e-5
28+
WEIGHTS=np.array([0.0,0.15,0.25,0.35,0.45,0.60])
29+
FEATURE_NAMES=['n_rows','p0_mean','pr_mean','abs_disagree_mean','disagree_std','signed_disagree_mean',
30+
'frac_abs_gt_03','frac_abs_gt_06','frac_abs_gt_10','p0_conf','pr_conf','turns','obj_len','n_objectives']
31+
32+
33+
def fit_expert(X,y,tr,va):
34+
m=LogisticRegression(C=.25,max_iter=300,solver='liblinear',random_state=SEED).fit(X[tr],y[tr])
35+
return np.clip(m.predict_proba(X[va])[:,1],EPS,1-EPS)
36+
37+
def ll(y,p): return float(log_loss(y,np.clip(p,EPS,1-EPS),labels=[0,1]))
38+
39+
def sf(p0,pr,idx,turns,obj_len,nobj):
40+
a=p0[idx]; b=pr[idx]; d=b-a
41+
return np.array([len(idx),a.mean(),b.mean(),np.mean(np.abs(d)),np.std(d),np.mean(d),
42+
np.mean(np.abs(d)>.03),np.mean(np.abs(d)>.06),np.mean(np.abs(d)>.10),
43+
np.mean(np.abs(a-.5)),np.mean(np.abs(b-.5)),np.mean(turns[idx]),np.mean(obj_len[idx]),float(nobj)],float)
44+
45+
def session_table(y,p0,pr,sess,obj,turns,obj_len,indices):
46+
rows=[]
47+
for s in np.unique(sess[indices]):
48+
loc_global=indices[np.where(sess[indices]==s)[0]]
49+
z=sf(p0,pr,loc_global,turns,obj_len,len(np.unique(obj[loc_global])))
50+
rows.append((s,loc_global,z))
51+
return rows
52+
53+
def choose_rule(y,p0,pr,rows):
54+
# Predeclared quantile thresholds; choose one feature + threshold + direction + two weights.
55+
Z=np.vstack([r[2] for r in rows])
56+
best=(1e99,None)
57+
for j,name in enumerate(FEATURE_NAMES):
58+
vals=Z[:,j]
59+
for q in (0.2,0.35,0.5,0.65,0.8):
60+
t=float(np.quantile(vals,q))
61+
for direction in ('low_more','high_more'):
62+
for w_more in (0.45,0.60):
63+
for w_less in (0.0,0.15,0.25,0.35):
64+
pred=np.empty(len(y)); mask=np.zeros(len(y),bool)
65+
for _,ix,z in rows:
66+
more=(z[j] <= t) if direction=='low_more' else (z[j] > t)
67+
w=w_more if more else w_less
68+
pred[ix]=(1-w)*p0[ix]+w*pr[ix]; mask[ix]=True
69+
v=ll(y[mask],pred[mask])
70+
if v<best[0]: best=(v,{'feature':name,'feature_index':j,'threshold':t,'direction':direction,'w_more':w_more,'w_less':w_less})
71+
return best
72+
73+
def apply_rule(p0,pr,rows,rule,n):
74+
q=np.empty(n)
75+
j=rule['feature_index']; t=rule['threshold']; direction=rule['direction']
76+
weights=[]
77+
for _,ix,z in rows:
78+
more=(z[j] <= t) if direction=='low_more' else (z[j] > t)
79+
w=rule['w_more'] if more else rule['w_less']; q[ix]=(1-w)*p0[ix]+w*pr[ix]; weights.extend([w]*len(ix))
80+
return q,np.asarray(weights)
81+
82+
def run(a):
83+
f=load_training(a.features,a.labels).reset_index(drop=True)
84+
cache={sid:load_transcript(a.transcripts/f'{sid}.csv') for sid in f.session_id.astype(str).unique()}
85+
rt=[]; rz=[]
86+
for i,r in f.iterrows():
87+
t,z=segmented_control(cache[str(r.session_id)],str(r.learning_objective),'related'); rt.append(t); rz.append(z)
88+
if (i+1)%2500==0: print('rows',i+1,flush=True)
89+
X0=build_v75(f,cache); Xr=build_control(rt,rz); y=f.target.to_numpy(int)
90+
obj=(f.learning_objective_id if 'learning_objective_id' in f else f.learning_objective).astype(str).to_numpy()
91+
sess=f.session_id.astype(str).to_numpy(); text=f.learning_objective.astype(str).to_numpy()
92+
turns=np.asarray([len(cache[str(s)]) for s in sess],float); obj_len=np.asarray([len(tokens(x)) for x in text],float)
93+
p97_all=np.zeros(len(y)); p104_all=np.zeros(len(y)); folds=[]
94+
for k,(tr,va) in enumerate(folds_from_groups(obj),1):
95+
p0=np.zeros(len(y)); pr=np.zeros(len(y))
96+
p0[va]=fit_expert(X0,y,tr,va); pr[va]=fit_expert(Xr,y,tr,va)
97+
ip0=np.zeros(len(y)); ipr=np.zeros(len(y))
98+
inner=GroupKFold(min(3,len(np.unique(obj[tr])))).split(np.zeros(len(tr)),y[tr],obj[tr])
99+
for itr_l,iva_l in inner:
100+
itr=tr[itr_l]; iva=tr[iva_l]
101+
ip0[iva]=fit_expert(X0,y,itr,iva); ipr[iva]=fit_expert(Xr,y,itr,iva)
102+
train_rows=session_table(y,ip0,ipr,sess,obj,turns,obj_len,tr)
103+
_,rule=choose_rule(y,ip0,ipr,train_rows)
104+
va_rows=session_table(y,p0,pr,sess,obj,turns,obj_len,va)
105+
q=np.empty(len(y)); qva,w=apply_rule(p0,pr,va_rows,rule,len(y)); q[va]=qva[va]
106+
q97=np.clip(.65*p0[va]+.35*pr[va],EPS,1-EPS); q104=np.clip(q[va],EPS,1-EPS)
107+
p97_all[va]=q97; p104_all[va]=q104
108+
folds.append({'fold':k,'v97':ll(y[va],q97),'v104':ll(y[va],q104),'gain':ll(y[va],q97)-ll(y[va],q104),
109+
'rule':rule,'mean_weight':float(np.mean(w))})
110+
print(folds[-1],flush=True)
111+
l97=ll(y,p97_all); l104=ll(y,p104_all); gain=l97-l104
112+
verdict='PROMOTE_TO_FOUR_WORLD_V104' if gain>=.002 else ('REFINE_SINGLE_SEPARATOR' if gain>=.0005 else 'SUPPRESS_SINGLE_SEPARATOR')
113+
out={'primary':'nested single session separator','v97':l97,'v104':l104,'gain_vs_v97':gain,'folds':folds,
114+
'decision':{'verdict':verdict,'precommit':'promote only if objective-cold gain vs frozen V97 >= 0.002'}}
115+
Path(a.out).write_text(json.dumps(out,indent=2)); print(json.dumps(out,indent=2),flush=True)
116+
if __name__=='__main__':
117+
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='v104_single_session_separator.json'); run(p.parse_args())

0 commit comments

Comments
 (0)