|
| 1 | +#!/usr/bin/env python3 |
| 2 | +"""V102: locate the resolution at which RELATED applicability actually lives. |
| 3 | +
|
| 4 | +Diagnostic only. Uses untouched outer objective-cold folds to generate V75/RELATED |
| 5 | +predictions, then measures label-informed oracle blend ceilings at progressively finer |
| 6 | +groupings. This does NOT define a deployable router; it decides what unit V103 should model. |
| 7 | +""" |
| 8 | +from __future__ import annotations |
| 9 | +import argparse, json |
| 10 | +from pathlib import Path |
| 11 | +import numpy as np |
| 12 | +from sklearn.linear_model import LogisticRegression |
| 13 | +from sklearn.metrics import log_loss |
| 14 | +from v71_mastery_events import load_transcript, tokens |
| 15 | +from v75_canonical_trajectory import load_training, SEED |
| 16 | +from v85_evidence_state import build_v75 |
| 17 | +from v93_shift_robust_validation import folds_from_groups |
| 18 | +from v94_related_control import segmented_control, build_control |
| 19 | + |
| 20 | +EPS=1e-5 |
| 21 | +GRID=np.array([0.,.15,.25,.35,.45,.60,1.0]) |
| 22 | + |
| 23 | +def fit(X,y,tr,va): |
| 24 | + m=LogisticRegression(C=.25,max_iter=300,solver='liblinear',random_state=SEED).fit(X[tr],y[tr]) |
| 25 | + return np.clip(m.predict_proba(X[va])[:,1],EPS,1-EPS) |
| 26 | + |
| 27 | +def ll(y,p): return float(log_loss(y,np.clip(p,EPS,1-EPS),labels=[0,1])) |
| 28 | + |
| 29 | +def oracle_group(y,p0,pr,groups): |
| 30 | + q=np.empty(len(y)); ws=[] |
| 31 | + for g in np.unique(groups): |
| 32 | + ix=np.where(groups==g)[0]; best=(1e99,0.,None) |
| 33 | + for w in GRID: |
| 34 | + z=(1-w)*p0[ix]+w*pr[ix]; v=ll(y[ix],z) |
| 35 | + if v<best[0]: best=(v,float(w),z) |
| 36 | + q[ix]=best[2]; ws.append(best[1]) |
| 37 | + return ll(y,q), float(np.mean(ws)), len(ws) |
| 38 | + |
| 39 | +def family(s): |
| 40 | + t=tokens(str(s)) |
| 41 | + return ' '.join(t[:3]) if t else '' |
| 42 | + |
| 43 | +def run(a): |
| 44 | + f=load_training(a.features,a.labels).reset_index(drop=True) |
| 45 | + cache={sid:load_transcript(a.transcripts/f'{sid}.csv') for sid in f.session_id.astype(str).unique()} |
| 46 | + rt=[]; rz=[] |
| 47 | + for i,r in f.iterrows(): |
| 48 | + t,z=segmented_control(cache[str(r.session_id)],str(r.learning_objective),'related'); rt.append(t); rz.append(z) |
| 49 | + if (i+1)%2500==0: print('rows',i+1) |
| 50 | + X0=build_v75(f,cache); Xr=build_control(rt,rz); y=f.target.to_numpy(int) |
| 51 | + obj=(f.learning_objective_id if 'learning_objective_id' in f else f.learning_objective).astype(str).to_numpy() |
| 52 | + sess=f.session_id.astype(str).to_numpy(); fam=np.asarray([family(x) for x in f.learning_objective.astype(str)]) |
| 53 | + p0=np.zeros(len(y)); pr=np.zeros(len(y)) |
| 54 | + for k,(tr,va) in enumerate(folds_from_groups(obj),1): |
| 55 | + p0[va]=fit(X0,y,tr,va); pr[va]=fit(Xr,y,tr,va); print('fold',k) |
| 56 | + v75=ll(y,p0); v97=ll(y,.65*p0+.35*pr) |
| 57 | + row_loss=np.minimum(-(y*np.log(p0)+(1-y)*np.log(1-p0)),-(y*np.log(pr)+(1-y)*np.log(1-pr))) |
| 58 | + row_oracle=float(np.mean(row_loss)) |
| 59 | + levels={} |
| 60 | + for name,g in [('family',fam),('objective',obj),('session',sess),('session_objective',np.char.add(np.char.add(sess,'|'),obj))]: |
| 61 | + val,mw,n=oracle_group(y,p0,pr,g); levels[name]={'oracle_ll':val,'gain_vs_v97':v97-val,'mean_best_weight':mw,'groups':n} |
| 62 | + # global best fixed weight ceiling |
| 63 | + best=min((ll(y,(1-w)*p0+w*pr),float(w)) for w in GRID) |
| 64 | + out={'v75':v75,'v97':v97,'global_grid_oracle':{'ll':best[0],'weight':best[1],'gain_vs_v97':v97-best[0]}, |
| 65 | + 'levels':levels,'row_endpoint_oracle':{'ll':row_oracle,'gain_vs_v97':v97-row_oracle}, |
| 66 | + 'decision_rule':'Choose the coarsest grouping whose oracle recovers a material fraction of the row endpoint oracle; V103 must model that unit using runtime-visible unlabeled features only.'} |
| 67 | + Path(a.out).write_text(json.dumps(out,indent=2)); print(json.dumps(out,indent=2)) |
| 68 | +if __name__=='__main__': |
| 69 | + 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='v102_applicability_resolution.json'); run(p.parse_args()) |
0 commit comments