(function(){'use strict';var registry={};window.LessonWidgets={register:function(type,mountFn){registry[type]=mountFn;},mount:function(el){if(!el||el.dataset.mounted)return;var type=el.getAttribute('data-widget-type');var fn=registry[type];if(!fn)return;var cfg={};try{cfg=JSON.parse(el.getAttribute('data-widget-config')||'{}');}catch(e){} el.dataset.mounted='1';try{fn(el,cfg);} catch(e){if(window.console)console.error('[lesson-widget] '+type+' failed',e);}},mountAll:function(root){var scope=root||document;var els=scope.querySelectorAll?scope.querySelectorAll('.lesson-widget'):[];for(var i=0;i"]/g,function(c){return({'&':'&','<':'<','>':'>','"':'"'})[c];});} function num(v){if(v==null||v===''||isNaN(v))return v;var n=+v;return(Math.round(n*100)/100).toString();} var u={P:P,esc:esc,num:num,code:function(t){return''+esc(t)+'';},rdf:function(data,cols,varName){var lines=cols.map(function(c){var vals=(data||[]).map(function(d){var v=d[c.key];return(v==null||v===''||isNaN(v))?'"'+String(v==null?'':v).replace(/"/g,'')+'"':(+v);}).join(', ');return' '+c.name+' = c('+vals+')';});return(varName||'df')+' <- data.frame(\n'+lines.join(',\n')+'\n)';},rdfCR:function(name,cols,rows){var lines=cols.map(function(c,ci){var vals=(rows||[]).map(function(r){var v=r[ci];return(v==null||v===''||isNaN(v))?'"'+String(v==null?'':v).replace(/"/g,'')+'"':(+v);}).join(', ');return' `'+c+'` = c('+vals+')';});return name+' <- data.frame(\n'+lines.join(',\n')+',\n check.names = FALSE, stringsAsFactors = FALSE\n)';},runnable:function(code,opts){opts=opts||{};var ec=esc(code),label=esc(opts.label||'Interactive R');return'
'+'
'+'
'+'R'+label+''+'
'+''+'
'+'
'+ec+'
'+'
'+''+''+'
'+'
'+'
'+'
'+'
';},previewSeed:function(innerSVG){return'
Preview - press Run to render with R
'+innerSVG;},btn:function(label,kind){var pri=kind==='primary';return'';},tbl:function(cols,rows,opts){opts=opts||{};var addC=opts.addCols||{},dropC=opts.dropCols||{},delR=opts.delRows||{},hi=opts.hi||{},head=opts.headBg||P.bg;var h='';h+='';cols.forEach(function(c){var bg=addC[c]?P.add:(dropC[c]?P.del:head);h+='';});h+='';rows.forEach(function(r,ri){var del=delR[ri];h+='';cols.forEach(function(c,ci){var k=ri+','+ci,bg=hi[k]||(addC[c]?P.add:(dropC[c]?P.del:'#fff')),v=r[ci];h+='';});h+='';});return h+'
'+esc(c)+'
'+(v==null?'NA':esc(v))+'
';},seg:function(items,cur){var h='
';items.forEach(function(it){var v=(it&&it.v!=null)?it.v:it,lab=(it&&it.label!=null)?it.label:v,on=(String(v)===String(cur));h+='';});return h+'
';},wireSeg:function(root,onPick){root.addEventListener('click',function(e){var b=e.target.closest('[data-val]');if(!b||!root.contains(b))return;var seg=b.parentNode;Array.prototype.forEach.call(seg.children,function(c){var on=c===b;c.style.background=on?P.acc:'transparent';c.style.color=on?'#fff':P.mut;});onPick(b.getAttribute('data-val'),b);});},plot:function(data,spec){spec=spec||{};var W=spec.w||460,H=spec.h||268,m={t:16,r:16,b:38,l:46},iw=W-m.l-m.r,ih=H-m.t-m.b;var geom=spec.geom||'point',pal=spec.palette||[P.acc,P.c0,P.c1,'#7a5ea3','#2f8f86','#b04a52'];function lin(d0,d1,r0,r1){if(d0===d1)d1=d0+1;return function(v){return r0+(v-d0)/(d1-d0)*(r1-r0);};} function ext(a){return[Math.min.apply(null,a),Math.max.apply(null,a)];} function frame(){return'';} function yax(sy,ticks){var a='';ticks.forEach(function(t){var y=sy(t);a+=''+num(t)+'';});return a;} function xlab(x,txt){return''+esc(txt)+'';} function axtitle(){return''+esc(spec.x||'')+''+esc(spec.y||'')+'';} var body='',extra='';if(geom==='point'){var xs=data.map(function(d){return+d.x;}),ys=data.map(function(d){return+d.y;}),xe=ext(xs),ye=ext(ys);var px=(xe[1]-xe[0])*0.08||1,py=(ye[1]-ye[0])*0.08||1,sx=lin(xe[0]-px,xe[1]+px,m.l,m.l+iw),sy=lin(ye[0]-py,ye[1]+py,m.t+ih,m.t);var gmap={},gi=0;data.forEach(function(d){var g=d.fill==null?'_':d.fill;if(!(g in gmap))gmap[g]=gi++;});body+=yax(sy,[ye[0],(ye[0]+ye[1])/2,ye[1]]);data.forEach(function(d){body+='';});if(spec.corr){var n=xs.length,mx=xs.reduce(function(a,b){return a+b;},0)/n,my=ys.reduce(function(a,b){return a+b;},0)/n,sxy=0,sxx=0,syy=0,i;for(i=0;ir = '+num(sxy/Math.sqrt(sxx*syy))+'';}}else if(geom==='line'){var lx=data.map(function(d){return+d.x;}),ly=data.map(function(d){return+d.y;}),lxe=ext(lx),lye=ext(ly),sxl=lin(lxe[0],lxe[1],m.l,m.l+iw),syl=lin(Math.min(0,lye[0]),lye[1],m.t+ih,m.t);body+=yax(syl,[Math.min(0,lye[0]),lye[1]])+'';data.forEach(function(d){body+='';});}else if(geom==='bar'||geom==='col'){var bv=data.map(function(d){return+d.y;}),ymax=(Math.max.apply(null,bv)||1)*1.12,syb=lin(0,ymax,m.t+ih,m.t),gap=iw/data.length,bw=gap*0.62;body+=yax(syb,[0,ymax/2,ymax]);data.forEach(function(d,i){var x=m.l+gap*i+(gap-bw)/2,y=syb(+d.y);body+=''+xlab(x+bw/2,d.x);});}else if(geom==='histogram'){var hv=data.map(function(d){return+(d.x!=null?d.x:d.y);}),he=ext(hv),bins=spec.bins||8,bw2=(he[1]-he[0])/bins||1,cts=[],b;for(b=0;b';});}else if(geom==='boxplot'){var grp={};data.forEach(function(d){var g=d.fill==null?(d.x==null?'all':d.x):d.fill;(grp[g]=grp[g]||[]).push(+d.y);});var keys=Object.keys(grp),all=data.map(function(d){return+d.y;}),ye4=ext(all),sy4=lin(ye4[0],ye4[1],m.t+ih,m.t),gw=iw/keys.length;body+=yax(sy4,[ye4[0],(ye4[0]+ye4[1])/2,ye4[1]]);keys.forEach(function(k,i){var vv=grp[k].slice().sort(function(a,b){return a-b;}),q=function(p){var idx=(vv.length-1)*p,lo=Math.floor(idx);return vv[lo]+(vv[Math.ceil(idx)]-vv[lo])*(idx-lo);};var cx=m.l+gw*i+gw/2,bw3=gw*0.4,q1=sy4(q(0.25)),q2=sy4(q(0.5)),q3=sy4(q(0.75)),lo=sy4(vv[0]),hi=sy4(vv[vv.length-1]);body+=''+xlab(cx,k);});} return''+frame()+body+axtitle()+extra+'';}};window.LessonWidgets.u=u;})();;(function(){'use strict';var u=window.LessonWidgets.u,P=u.P;var EPS={none:0,small:0.5,large:0.9};var W1=2.5,W2=2.5;var PT={x1:0.35,x2:0.30};function prob(x1,x2){return 1/(1+Math.exp(-(W1*x1+W2*x2)));} function mount(el,cfg){cfg=cfg||{};var lvl='none';el.style.cssText='border:1px solid '+P.line+';border-radius:12px;background:#fff;padding:16px 17px';el.innerHTML='
'+u.seg([{v:'none',label:'Original'},{v:'small',label:'+ tiny perturbation'},{v:'large',label:'+ larger'}],'none')+'
'+'
'+'
'+ u.runnable(rcode(),{label:'An adversarial (FGSM) perturbation flips a logistic prediction, base R'});var plot=el.querySelector('.ad-plot'),read=el.querySelector('.ad-read');var W=300,H=200,PAD=16,LO=-1.6,HI=1.6;function px(x){return PAD+(x-LO)/(HI-LO)*(W-2*PAD);} function py(y){return(H-PAD)-(y-LO)/(HI-LO)*(H-2*PAD);} function draw(){var eps=EPS[lvl];var ax1=PT.x1-eps,ax2=PT.x2-eps;var p0=prob(PT.x1,PT.x2),p1=prob(ax1,ax2);var bx1=px(LO),by1=py(-LO),bx2=px(HI),by2=py(-HI);var svg='';svg+='';svg+='';svg+='';svg+='class 1';svg+='class 0';svg+='';if(eps>0){svg+='';svg+='';} svg+='feature space (attack steps toward the boundary)';plot.innerHTML=svg;read.innerHTML=eps===0?'The input sits in class 1 with probability '+p0.toFixed(2)+', confidently correct.':'A perturbation of just '+eps+' per feature '+(p1>0.5?'moves the point toward the boundary (prob '+p1.toFixed(2)+'), still class 1.':'pushes it across the boundary: prediction flips to class 0 at probability '+p1.toFixed(2)+', confidently WRONG, from a nudge you can barely see.');} u.wireSeg(el.querySelector('.ad-seg'),function(v){lvl=v;draw();});draw();} function rcode(){return['# Adversarial example: a tiny perturbation flips a confident logistic prediction. Base R.','set.seed(3)','n <- 800; X <- matrix(rnorm(n * 2), n, 2); colnames(X) <- c("x1", "x2")','y <- rbinom(n, 1, plogis(2.5 * X[, 1] + 2.5 * X[, 2]))','fit <- glm(y ~ x1 + x2, binomial, data = data.frame(y, X))','w <- coef(fit)[c("x1", "x2")]','','pt <- data.frame(x1 = 0.35, x2 = 0.30) # confidently classified as class 1','eps <- 0.5 # a tiny per-feature budget','adv <- data.frame(x1 = pt$x1 - eps * sign(w["x1"]), # FGSM: step against the gradient',' x2 = pt$x2 - eps * sign(w["x2"]))','round(c(orig_prob = predict(fit, pt, type = "response"),',' adv_prob = predict(fit, adv, type = "response"),',' Linf_perturb = eps), 3)','#> orig_prob adv_prob Linf_perturb','#> 0.858 0.318 0.500 # prediction flips 1 -> 0'].join('\n');} window.LessonWidgets.register('adversarial-perturb',mount);})();;(function(){'use strict';var INK='#131720',MUT='#677084',ACC='#1f7a55',LINE='#c5cdda';var PC={Thought:'#2563a8',Action:'#1f7a55',Observation:'#b5631a',Answer:'#8a46b0'};function mount(el,cfg){cfg=cfg||{};var trace=(cfg.trace&&cfg.trace.length)?cfg.trace:[{p:'Thought',t:'The user wants the overall churn rate; I should query the data.'},{p:'Action',t:'run_sql("SELECT AVG(churned) FROM customers")'},{p:'Observation',t:'0.062'},{p:'Thought',t:'6.2% overall. They also asked which plan churns worst.'},{p:'Action',t:'run_sql("... GROUP BY plan ORDER BY churn DESC LIMIT 1")'},{p:'Observation',t:'plan = "basic", churn = 0.11'},{p:'Answer',t:'Churn is 6.2% overall; the basic plan is worst at 11%.'}];var i=0;el.innerHTML='
'+'
'+'
'+'
'+' '+''+'
';var cycle=el.querySelector('.al-cycle'),traceEl=el.querySelector('.al-trace'),stepBtn=el.querySelector('.al-step'),resetBtn=el.querySelector('.al-reset');cycle.innerHTML=['Thought','Action','Observation'].map(function(p){return''+p+'';}).join('')+'';var nodes=cycle.querySelectorAll('.al-node');function render(){var html='',k;for(k=0;k'+''+s.p+' '+''+s.t+'';} traceEl.innerHTML=html||'
Press Step to watch the agent reason, act, and observe in a loop until it can answer.
';var active=i>0?trace[i-1].p:null;Array.prototype.forEach.call(nodes,function(n){var on=n.getAttribute('data-p')===active;n.style.borderColor=on?PC[active]:LINE;n.style.color=on?PC[active]:MUT;});stepBtn.disabled=i>=trace.length;stepBtn.style.opacity=i>=trace.length?'.5':'1';} stepBtn.addEventListener('click',function(){if(i=0,hy=b.indexOf(y)>=0;if(hx)X++;if(hy)Y++;if(hx&&hy)XY++;});var supp=XY/n,conf=X?XY/X:0,lift=(Y/n)?conf/(Y/n):0;return{supp:supp,conf:conf,lift:lift};} el.style.cssText='border:1px solid '+P.line+';border-radius:12px;background:#fff;padding:16px 17px';el.innerHTML='
pick a rule
'+'
'+'
'+'
'+ u.runnable(rcode(),{label:'Compute support, confidence and lift in R'});var seg=el.querySelector('.ar-seg'),bars=el.querySelector('.ar-bars'),read=el.querySelector('.ar-read');RULES.forEach(function(r,i){var b=document.createElement('button');b.innerHTML=r[0]+' → '+r[1];b.style.cssText='font:600 12px IBM Plex Sans,sans-serif;border:1px solid '+P.line+';border-radius:8px;padding:6px 11px;cursor:pointer';b.addEventListener('click',function(){ri=i;draw();});seg.appendChild(b);});function bar(lab,v,max,fmt){return'
'+lab+''+''+''+fmt+'
';} function draw(){seg.querySelectorAll('button').forEach(function(x,i){var on=i===ri;x.style.background=on?P.ink:'#fff';x.style.color=on?'#fff':P.body;});var r=RULES[ri],m=metrics(r[0],r[1]);bars.innerHTML=bar('support',m.supp,1,(m.supp*100).toFixed(0)+'%')+bar('confidence',m.conf,1,(m.conf*100).toFixed(0)+'%')+bar('lift',m.lift,3,m.lift.toFixed(2));read.innerHTML='Buyers of '+r[0]+' take '+r[1]+' '+(m.conf*100).toFixed(0)+'% of the time, and lift '+m.lift.toFixed(2)+' means that is '+(m.lift>=1?(m.lift.toFixed(1)+'x more likely than chance - a real association worth acting on'):'no more likely than chance - not a useful rule')+'.';} draw();} function rcode(){return['# Association rules score "if X then Y" with three numbers.','baskets <- list(c("bread","butter","milk"), c("bread","butter"), c("bread","milk"),',' c("beer","chips"), c("bread","butter","jam"), c("beer","chips"))','has <- function(it) sapply(baskets, function(b) it %in% b)','n <- length(baskets)','','support <- mean(has("bread") & has("butter")) # X and Y together','confidence <- sum(has("bread") & has("butter")) / sum(has("bread"))','lift <- confidence / mean(has("butter")) # > 1 = real association','c(support = support, confidence = confidence, lift = lift)'].join('\n');} window.LessonWidgets.register('assoc-rules',mount);})();;(function(){'use strict';var u=window.LessonWidgets.u;var LIGHT={ink:'#131720',body:'#434b59',mut:'#677084',faint:'#97a0b2',line:'#d8dee9',grid:'#eef1f6',acc:'#1f7a55',c0:'#2563a8',bad:'#c2410c',panel:'#ffffff'};var DARK={ink:'#eef4fb',body:'#c3d1e3',mut:'#93a4bb',faint:'#6f8299',line:'rgba(255,255,255,.24)',grid:'rgba(255,255,255,.10)',acc:'#46c08a',c0:'#7fb2ea',bad:'#f4805a',panel:'#101c2b'};function lumOf(c){c=String(c).trim();var r,g,b,m;if(c.charAt(0)==='#'){if(c.length===4){r=parseInt(c[1]+c[1],16);g=parseInt(c[2]+c[2],16);b=parseInt(c[3]+c[3],16);} else{r=parseInt(c.substr(1,2),16);g=parseInt(c.substr(3,2),16);b=parseInt(c.substr(5,2),16);}}else if((m=c.match(/rgba?\(([^)]+)\)/))){var p=m[1].split(',');r=+p[0];g=+p[1];b=+p[2];} else return 1;return(0.2126*r+0.7152*g+0.0722*b)/255;} function isDark(el){try{var v=getComputedStyle(el).getPropertyValue('--lm-panel');if(v&&v.trim())return lumOf(v)<0.45;}catch(e){} return!!(document.documentElement&&document.documentElement.classList.contains('dark'));} function palette(el){return isDark(el)?DARK:LIGHT;} function mulberry32(a){return function(){a|=0;a=a+0x6D2B79F5|0;var t=Math.imul(a^a>>>15,1|a);t=t+Math.imul(t^t>>>7,61|t)^t;return((t^t>>>14)>>>0)/4294967296;};} function gaussian(rnd){var a=1-rnd(),b=rnd();return Math.sqrt(-2*Math.log(a))*Math.cos(2*Math.PI*b);} function lgamma(x){var c=[76.18009172947146,-86.50532032941677,24.01409824083091,-1.231739572450155,0.1208650973866179e-2,-0.5395239384953e-5];var y=x,t=x+5.5,s=1.000000000190015;t-=(x+0.5)*Math.log(t);for(var j=0;j<6;j++)s+=c[j]/++y;return-t+Math.log(2.5066282746310005*s/x);} function betacf(a,b,x){var FPMIN=1e-300,qab=a+b,qap=a+1,qam=a-1;var c=1,d=1-qab*x/qap;if(Math.abs(d)=1)return 1;var bt=Math.exp(lgamma(a+b)-lgamma(a)-lgamma(b)+a*Math.log(x)+b*Math.log(1-x));return(x<(a+1)/(a+b+2))?bt*betacf(a,b,x)/a:1-bt*betacf(b,a,1-x)/b;} function pt(t,df){var p=0.5*ibeta(df/2,0.5,df/(df+t*t));return t>0?1-p:p;} function qt(p,df){var lo=-60,hi=60,i;for(i=0;i<160;i++){var mid=(lo+hi)/2;if(pt(mid,df)=0.9499)return 1;return 1.959963985/qnorm((1+Math.max(0.001,cov))/2);} function inv(A){var p=A.length,M=[],i,j,k;for(i=0;iMath.abs(M[piv][i]))piv=k;var tmp=M[i];M[i]=M[piv];M[piv]=tmp;var dd=M[i][i]||1e-300;for(j=0;j<2*p;j++)M[i][j]/=dd;for(k=0;k=0;i--){k=order[i];zk=z[k];w=Math.exp(beta*zk);s0+=w;s1+=w*zk;s2+=w*zk*zk;if(event[k]){m1=s1/s0;g+=zk-m1;h+=s2/s0-m1*m1;}} if(h<1e-12)break;step=g/h;beta+=step;if(Math.abs(step)<1e-10)break;} s0=0;s1=0;s2=0;var I=0;for(i=n-1;i>=0;i--){k=order[i];zk=z[k];w=Math.exp(beta*zk);s0+=w;s1+=w*zk;s2+=w*zk*zk;if(event[k]){m1=s1/s0;I+=s2/s0-m1*m1;}} return{beta:beta,se:I>0?1/Math.sqrt(I):NaN};} function cindex(time,event,risk){var n=time.length,conc=0,disc=0,tie=0,i,j;for(i=0;irisk[j])conc++;else if(risk[i]d.adm){t=d.adm;event[i]=0;}else event[i]=1;time[i]=t;}}}};function measure(spec,s,n,sims,seed,keep,d){var rnd=mulberry32(seed),truth=spec.truth(s,d);var hits=0,fitSum=0,widthSum=0,cis=[],i;if(spec.fit==='cox'){var time=new Float64Array(n),event=new Int8Array(n),risk=new Float64Array(n);var cSum=0,cN=0,j;for(i=0;i1?'
':'')+'
'+''+'
'+'
'+'
';var segBox=el.querySelector('.ad-seg'),plot=el.querySelector('.ad-plot'),read=el.querySelector('.ad-read'),lab=el.querySelector('.ad-lab'),slider=el.querySelector('.ad-sev'),rbox=el.querySelector('.ad-r');if(segBox){segBox.innerHTML=u.seg(list.map(function(k){return{v:k,label:SPECS[k].short};}),which);u.wireSeg(segBox,function(v){which=v;render(true);});} function series(key){if(cache[key])return cache[key];var spec=SPECS[key],n=Math.max(12,+cfg.n||spec.n),sims=Math.max(150,+cfg.sims||spec.sims);var d=spec.design(mulberry32(seed^0x5f3759d),n),out=[],i;for(i=0;i'+u.esc(S.spec.dial(s))+'';plot.innerHTML=svg(S,step,P,bars);read.innerHTML=verdict(which,S,step,P);if(newR||!rbox.firstChild)rbox.innerHTML=u.runnable(RCODE[which](),{label:'Run the same experiment in R'});} slider.addEventListener('input',function(){step=+slider.value;render(false);});var wasDark=isDark(el);if(window.MutationObserver){var mo=new MutationObserver(function(){if(!document.contains(el)){mo.disconnect();return;} var now=isDark(el);if(now!==wasDark){wasDark=now;render(false);}});mo.observe(document.documentElement,{attributes:true,attributeFilter:['class']});} render(true);} function svg(S,step,P,bars){var W=520,mA={l:46,r:16,t:42},hA=122;var yTop=mA.t,yBot=mA.t+hA,levels=S.rows.length;var fitName=S.spec.fit==='cox'?'Concordance (C)':'R-squared';var HALO='stroke="'+P.panel+'" stroke-width="4.6" paint-order="stroke" stroke-linejoin="round"';function sx(i){return mA.l+i/(levels-1)*(W-mA.l-mA.r);} function sy(v){return yBot-v*hA;} var g='',i;[0,0.25,0.5,0.75,1].forEach(function(v){g+=''+''+(v*100)+'%';});g+='';var cov=[],fit=[];for(i=0;i'+''+''+''+'';var late=step>(levels-1)*0.62,yc=sy(S.rows[step].cov);var yLab=(yc-12>=yTop+10)?yc-11:yc+19;g+=''+ (100*S.rows[step].cov).toFixed(1)+'%';g+='Interval coverage (promised 95%)'+''+fitName+''+''+ u.esc(S.sims.toLocaleString()+' studies behind every point, n = '+S.n)+''+''+'none'+'severe'+''+ u.esc(S.spec.dialName)+'';var row=S.rows[step],cis=row.cis,truth=row.truth;var yB=yBot+54,hB=Math.max(96,bars*4.2),mB={l:46,r:16};var lows=[],highs=[];cis.forEach(function(c){lows.push(c[0]);highs.push(c[1]);});lows.sort(function(a,b){return a-b;});highs.sort(function(a,b){return a-b;});var lo=Math.min(lows[Math.floor(lows.length*0.08)],truth);var hi=Math.max(highs[Math.floor(highs.length*0.92)],truth);var pad=(hi-lo)*0.12||1;lo-=pad;hi+=pad;function bx(v){return mB.l+(Math.max(lo,Math.min(hi,v))-lo)/(hi-lo)*(W-mB.l-mB.r);} g+=''+ cis.length+' of those studies: the 95% interval for '+u.esc(S.spec.estimand)+'';var miss=0;cis.forEach(function(c,k){var yy=yB+6+k*((hB-12)/Math.max(1,cis.length-1));var col=c[2]?P.mut:P.bad;if(!c[2])miss++;g+='';if(c[0]';if(c[1]>hi)g+='';});g+=''+'true value'+''+ miss+' of '+cis.length+' miss';var H=yB+hB+24;return''+g+'';} function verdict(key,S,step,P){var a=S.rows[0],b=S.rows[step];var pc=function(v){return(100*v).toFixed(1)+'%';};var covTx=''+pc(b.cov)+'';var fitName=S.spec.fit==='cox'?'Concordance':'R-squared';var fitTx=''+b.fit.toFixed(3)+'';if(step===0){return'With the assumption satisfied, '+covTx+' of the 95% intervals contain the true value, which is what 95% is supposed to mean. '+ fitName+' is '+fitTx+'. Drag the dial and watch which of those two numbers moves.';} var common='Coverage is now '+covTx+' against the 95% it promises. '+fitName+' went from '+a.fit.toFixed(3)+' to '+fitTx+'. ';var tooNarrow=understate(b.cov).toFixed(1);if(key==='multicollinearity'){return'Coverage is '+covTx+'. It has not moved and it will not: collinearity biases nothing. What moved is the WIDTH, now '+ (b.width/a.width).toFixed(1)+'x what it was with uncorrelated predictors. The interval is telling the truth about how little this data can separate two variables that carry the same information. '+'A wide honest interval is a result to report, not a violation to fix.';} if(key==='normality'){return common+'This is the mildest of the seven. An estimate is an average of many draws, and averages are close to normal whatever the draws look like, '+'so the interval survives an error distribution that looks nothing like a bell. Non-normal residuals are a reason to look for outliers and for the wrong functional form. On their own they are not a reason to distrust the interval.';} if(key==='linearity'){return common+'The line still summarises the cloud, which is why the fit statistic stays calm. It is the PREDICTION at the top of the range that is wrong, '+'and no standard error repairs that: the model is estimating the wrong quantity, not estimating the right one badly.';} if(key==='proportional-hazards'){return common+'The model still ranks patients well, so nothing in the fit complains. But one hazard ratio is being asked to describe an effect that changes over follow-up, '+'so the interval quoted for the early period misses it '+pc(1-b.cov)+' of the time. Split the follow-up, or model the interaction with time, and quote both periods.';} if(key==='independence'){return common+'Every row is being counted as fresh information when rows inside a group are partly the same information. The intervals are about '+tooNarrow+'x too narrow. This is the one an ordinary robust standard error does not repair, because it also assumes independent observations.';} if(key==='autocorrelation'){return common+'Neighbouring errors move together, so 60 observations carry far less than 60 observations worth of information. The intervals are about '+tooNarrow+'x too narrow, and note that the fit statistic went UP: a smooth error series flatters R-squared at the same time as it destroys the interval.';} return common+'The estimate is roughly right and the interval around it is about '+tooNarrow+'x too narrow. '+'That is a standard-error problem, not a model problem: the fit is fine and the uncertainty statement is not.';} function head(txt){return['# '+txt,'# sev runs 0 (assumption holds) to 1 (severe). Change it and run again.','set.seed(11)'];} var RCODE={normality:function(){return head('Skewed errors. Does the 95% interval for the slope still cover the truth?').concat(['n <- 30; sims <- 2000; sev <- 1','x <- as.numeric(scale(rnorm(n)))','sg <- max(1e-6, 0.85 * sev)','mu <- exp(sg^2 / 2); sdl <- sqrt((exp(sg^2) - 1) * exp(sg^2))','','one <- function() {',' e <- (exp(sg * rnorm(n)) - mu) / sdl # standardised lognormal: mean 0, sd 1',' y <- x + e',' m <- lm(y ~ x)',' ci <- confint(m)["x", ]',' c(covered = ci[1] < 1 && 1 < ci[2], r2 = summary(m)$r.squared)','}','res <- t(replicate(sims, one()))','round(c(coverage = mean(res[, "covered"]), r_squared = mean(res[, "r2"])), 3)']).join('\n');},heteroskedasticity:function(){return head('Error spread that grows with x. The fit is fine; the interval is not.').concat(['n <- 60; sims <- 2000; sev <- 1','x <- as.numeric(scale(rnorm(n)))','w <- exp(1.1 * sev * x); w <- w / sqrt(mean(w^2)) # average error variance stays 1','','one <- function() {',' y <- x + rnorm(n, sd = w)',' m <- lm(y ~ x)',' ci <- confint(m)["x", ]',' c(covered = ci[1] < 1 && 1 < ci[2], r2 = summary(m)$r.squared)','}','res <- t(replicate(sims, one()))','round(c(coverage = mean(res[, "covered"]), r_squared = mean(res[, "r2"])), 3)']).join('\n');},independence:function(){return head('Observations clustered in groups. Coverage falls; R-squared does not.').concat(['n <- 60; m <- 5; k <- n / m; sims <- 2000; sev <- 1','rho <- 0.6 * sev # intraclass correlation','cl <- rep(1:k, each = m)','x <- as.numeric(scale(rep(rnorm(k), each = m))) # the predictor varies BETWEEN groups','','one <- function() {',' y <- x + sqrt(rho) * rnorm(k)[cl] + sqrt(1 - rho) * rnorm(n)',' fm <- lm(y ~ x)',' ci <- confint(fm)["x", ]',' c(covered = ci[1] < 1 && 1 < ci[2], r2 = summary(fm)$r.squared)','}','res <- t(replicate(sims, one()))','round(c(coverage = mean(res[, "covered"]), r_squared = mean(res[, "r2"])), 3)','round(1 + (m - 1) * rho, 2) # design effect: the factor the variance is out by']).join('\n');},autocorrelation:function(){return head('Errors correlated with their own past, fitted against a trend.').concat(['n <- 60; sims <- 2000; sev <- 1','phi <- 0.92 * sev','x <- as.numeric(scale(seq_len(n)))','','one <- function() {',' e <- as.numeric(arima.sim(list(ar = phi), n = n, sd = sqrt(1 - phi^2)))',' y <- x + e',' m <- lm(y ~ x)',' ci <- confint(m)["x", ]',' c(covered = ci[1] < 1 && 1 < ci[2], r2 = summary(m)$r.squared)','}','res <- t(replicate(sims, one()))','round(c(coverage = mean(res[, "covered"]), r_squared = mean(res[, "r2"])), 3)']).join('\n');},multicollinearity:function(){return head('Two near-duplicate predictors. Watch the width, not the coverage.').concat(['n <- 60; sims <- 2000; sev <- 1','r <- 0.995 * sev','a <- as.numeric(scale(rnorm(n))); b <- as.numeric(scale(rnorm(n)))','b <- as.numeric(scale(residuals(lm(b ~ a)))) # make b independent of a','x1 <- a; x2 <- r * a + sqrt(1 - r^2) * b','','one <- function() {',' y <- x1 + x2 + rnorm(n, sd = sqrt(1 + r)) # holds R-squared still as r moves',' m <- lm(y ~ x1 + x2)',' ci <- confint(m)["x1", ]',' c(covered = ci[1] < 1 && 1 < ci[2], width = unname(ci[2] - ci[1]), r2 = summary(m)$r.squared)','}','res <- t(replicate(sims, one()))','round(c(coverage = mean(res[, "covered"]),',' mean_width = mean(res[, "width"]),',' r_squared = mean(res[, "r2"])), 3)']).join('\n');},linearity:function(){return head('A curved truth fitted with a straight line. The prediction at the top is the casualty.').concat(['n <- 60; sims <- 2000; sev <- 1','x <- 0.2 + 2 * runif(n); q <- x^2','zx <- as.numeric(scale(x)); zq <- as.numeric(scale(q))','top <- 2.2','truth <- 2 * ((1 - sev) * (top - mean(x)) / sd(x) +',' sev * (top^2 - mean(q)) / sd(q))','','one <- function() {',' y <- 2 * ((1 - sev) * zx + sev * zq) + rnorm(n)',' m <- lm(y ~ x)',' ci <- predict(m, data.frame(x = top), interval = "confidence")',' c(covered = ci[1, "lwr"] < truth && truth < ci[1, "upr"], r2 = summary(m)$r.squared)','}','res <- t(replicate(sims, one()))','round(c(coverage = mean(res[, "covered"]), r_squared = mean(res[, "r2"])), 3)']).join('\n');},'proportional-hazards':function(){return head('A hazard ratio that changes halfway through follow-up.').concat(['library(survival)','n <- 200; sims <- 400; sev <- 1','z <- rep(0:1, each = n / 2)','cut <- 0.5; adm <- 2.0','late <- 0.5 + 0.5 * sev # the early HR stays 0.50 throughout','','one <- function() {',' l1 <- ifelse(z == 1, 0.5, 1); l2 <- ifelse(z == 1, late, 1)',' E <- rexp(n)',' tt <- ifelse(E < l1 * cut, E / l1, cut + (E - l1 * cut) / l2)',' d <- as.integer(tt <= adm); tt <- pmin(tt, adm)',' m <- coxph(Surv(tt, d) ~ z)',' ci <- confint(m)',' c(covered = ci[1] < log(0.5) && log(0.5) < ci[2],',' conc = unname(summary(m)$concordance[1]))','}','res <- t(replicate(sims, one()))','round(c(coverage = mean(res[, "covered"]), concordance = mean(res[, "conc"])), 3)']).join('\n');}};window.LessonWidgets.register('assumption-dial',mount);})();;(function(){'use strict';var u=window.LessonWidgets.u,P=u.P;function rng(seed){var s=seed>>>0;return function(){s=(s*1664525+1013904223)>>>0;return s/4294967296;};} function gauss(r){return Math.sqrt(-2*Math.log(Math.max(r(),1e-9)))*Math.cos(6.2831853*r());} function build(){var r=rng(5),pts=[],i,x;for(i=0;i<90;i++){x=1.7*gauss(r);pts.push([x,0.7*x+0.18*gauss(r)]);} pts.push([1.6,-1.6]);return pts;} function pca1(pts){var n=pts.length,mx=0,my=0,i;for(i=0;i'+'
'+'
'+ u.runnable(rcode(),{label:'Autoencoder anomaly detection = PCA reconstruction error, base R'});var plot=el.querySelector('.ae-plot'),read=el.querySelector('.ae-read');var W=340,H=240,PAD=16,LO=-3,HI=3;function sx(x){return PAD+(x-LO)/(HI-LO)*(W-2*PAD);} function sy(y){return H-PAD-(y-LO)/(HI-LO)*(H-2*PAD);} var median=(function(){var e=model.pts.slice(0,anom).map(function(o){return o.err;}).sort(function(a,b){return a-b;});return e[Math.floor(e.length/2)];})();function draw(){var pIdx=sel==='anomaly'?anom:7;var m=model.mean,v=model.v,L=4.2;var line='';var resid=model.pts.map(function(o,i){var p=pts[i];var bold=i===pIdx;return'';}).join('');var dots=pts.map(function(p,i){var isSel=i===pIdx,isA=i===anom;return'';}).join('');plot.innerHTML=''+ line+resid+dots+'dashed = bottleneck manifold'+'red = reconstruction error';var err=model.pts[pIdx].err;read.innerHTML='The '+(sel==='anomaly'?'anomaly':'normal point')+' reconstructs with squared error '+err.toFixed(2)+', against a typical normal error of about '+median.toFixed(3)+'. '+ (sel==='anomaly'?'It sits off the manifold the bottleneck learned, so it cannot be rebuilt from one component: a large error flags it.':'It lies on the manifold, so one component rebuilds it almost perfectly: a tiny error, no flag.');} u.wireSeg(el.querySelector('.ae-seg'),function(v){sel=v;draw();});draw();} function rcode(){return['# Autoencoder anomaly detection: reconstruct through a 1-component (PCA) bottleneck. Base R.','# A linear autoencoder IS PCA; error = distance to the learned manifold (here a line).','set.seed(1)','n <- 200','x <- rnorm(n); Y <- cbind(x, 0.7*x + rnorm(n, 0, 0.15)) # 2D data hugging a line','Y <- rbind(Y, c(1.5, -1.5)) # anomaly far off the line (row 201)','pca <- prcomp(Y, center = TRUE, rank. = 1) # encoder: keep 1 of 2 dims (bottleneck)','recon <- sweep(predict(pca) %*% t(pca$rotation[, 1, drop = FALSE]), 2, pca$center, "+") # decode','err <- rowSums((Y - recon)^2) # reconstruction error per point','round(c(median_normal = median(err[1:200]), anomaly = err[201]), 3)','#> median_normal anomaly','#> 0.007 4.346'].join('\n');} window.LessonWidgets.register('autoencoder-recon',mount);})();;(function(){'use strict';var u=window.LessonWidgets.u,P=u.P;var RATES=[0.30,0.50,0.55],BEST=0.55,T=400;function rng(seed){var s=seed>>>0;return function(){s=(s*1664525+1013904223)>>>0;return s/4294967296;};} function simulate(strategy){var r=rng(strategy==='thompson'?7:19),spare=null;function rnorm(){if(spare!==null){var v=spare;spare=null;return v;}var a=Math.max(r(),1e-12),b=r(),m=Math.sqrt(-2*Math.log(a));spare=m*Math.sin(2*Math.PI*b);return m*Math.cos(2*Math.PI*b);} var K=RATES.length,succ=[0,0,0],n=[0,0,0],regret=0,curve=[];for(var t=0;tbm){bm=mean;pick=k;}}}}else{var best=-1;for(var k2=0;k2best){best=draw;pick=k2;}}} var reward=r()'+'
'+'
'+ u.runnable(rcode(),{label:'A three-arm bandit: epsilon-greedy vs Thompson, base R'});var plot=el.querySelector('.ba-plot'),read=el.querySelector('.ba-read');var eg=simulate('egreedy'),th=simulate('thompson');var YMAX=Math.max(eg.regret,th.regret)*1.05;var W=340,H=185,PAD=30;function px(t){return PAD+(t/T)*(W-PAD-8);} function py(v){return(H-22)-(v/YMAX)*(H-34);} function path(c){return c.map(function(v,t){return px(t).toFixed(1)+','+py(v).toFixed(1);}).join(' ');} function draw(){var sel=strat==='thompson'?th:eg,other=strat==='thompson'?eg:th;plot.innerHTML=''+''+''+'cumulative regret'+'round (1 to '+T+')';read.innerHTML=''+(strat==='thompson'?'Thompson sampling':'epsilon-greedy')+' ends '+T+' rounds with total regret '+sel.regret.toFixed(1)+' (the dashed line is the other strategy at '+other.regret.toFixed(1)+'). '+ (strat==='thompson'?'Its curve bends toward flat: once an arm looks clearly worse, Thompson almost stops pulling it.':'It keeps spending a fixed 10% of pulls exploring at random forever, so its regret keeps climbing even after the best arm is obvious.');} u.wireSeg(el.querySelector('.ba-seg'),function(v){strat=v;draw();});draw();} function rcode(){return['# Three-arm bandit: epsilon-greedy vs Thompson sampling, scored by cumulative regret. Base R.','rates <- c(0.30, 0.50, 0.55); best <- max(rates); T <- 400','','run <- function(strategy) {',' succ <- rep(0, 3); n <- rep(0, 3); regret <- 0',' for (t in 1:T) {',' if (strategy == "egreedy") {',' pick <- if (runif(1) < 0.1) sample(3, 1) else which.max(ifelse(n > 0, succ / n, 1))',' } else {',' pick <- which.max(rbeta(3, succ + 1, n - succ + 1)) # Thompson: draw from each arm\'s Beta',' }',' reward <- rbinom(1, 1, rates[pick])',' succ[pick] <- succ[pick] + reward; n[pick] <- n[pick] + 1',' regret <- regret + (best - rates[pick])',' }',' c(regret = regret, pulls_best = n[which.max(rates)])','}','set.seed(7)','round(rbind(egreedy = run("egreedy"), thompson = run("thompson")), 1)','# Thompson ends with lower regret and far more pulls on the best arm.'].join('\n');} window.LessonWidgets.register('bandit-explore',mount);})();;(function(){'use strict';var u=window.LessonWidgets.u,P=u.P;function mount(el,cfg){cfg=cfg||{};var st={pm:0,ps:1.0,dm:3,n:10};var DSD=2;el.style.cssText='border:1px solid '+P.line+';border-radius:12px;background:#fff;padding:16px 17px';el.innerHTML='
'+ row('prior mean','pm',-4,6,0.5,st.pm)+ row('prior confidence','ps',0.4,3,0.1,st.ps,true)+ row('data average','dm',-4,6,0.5,st.dm)+ row('data points n','n',1,200,1,st.n)+'
'+ u.runnable(rcode(),{label:'The conjugate normal-normal update in base R'});var plot=el.querySelector('.bu-plot'),read=el.querySelector('.bu-read');function row(lab,key,lo,hi,step,val,inv){return'';} function dnorm(x,m,s){return Math.exp(-(x-m)*(x-m)/(2*s*s))/(s*Math.sqrt(2*Math.PI));} function draw(){var priorVar=st.ps*st.ps,dataVar=DSD*DSD/st.n;var postVar=1/(1/priorVar+1/dataVar);var postM=postVar*(st.pm/priorVar+st.dm/dataVar);var postS=Math.sqrt(postVar),likS=Math.sqrt(dataVar);var W=440,H=190,m={l:8,r:8,t:10,b:22},iw=W-m.l-m.r,ih=H-m.t-m.b;var lo=Math.min(st.pm,st.dm,postM)-4,hi=Math.max(st.pm,st.dm,postM)+4;var peak=Math.max(dnorm(st.pm,st.pm,st.ps),dnorm(st.dm,st.dm,likS),dnorm(postM,postM,postS));function sx(x){return m.l+(x-lo)/(hi-lo)*iw;} function sy(y){return m.t+ih-y/(peak*1.08)*ih;} function curve(mn,sd,col,fill){var pts='';for(var i=0;i<=80;i++){var x=lo+(hi-lo)*i/80;pts+=sx(x).toFixed(1)+','+sy(dnorm(x,mn,sd)).toFixed(1)+' ';} var area=fill?'':'';return area+'';} var svg='';svg+='';svg+=curve(st.pm,st.ps,P.mut,false);svg+=curve(st.dm,likS,P.c1,false);svg+=curve(postM,postS,P.acc,true);var lg=[['prior',P.mut],['likelihood (data)',P.c1],['posterior',P.acc]];lg.forEach(function(o,i){svg+=''+o[0]+'';});svg+='';plot.innerHTML=svg;var pull=((postM-st.pm)/(st.dm-st.pm||1)*100);read.innerHTML='posterior μ '+postM.toFixed(2)+', sd '+postS.toFixed(2)+'. '+'The posterior sits '+(isFinite(pull)?Math.max(0,Math.min(100,Math.round(pull))):0)+'% of the way from your prior toward the data. '+ (st.n>=60?'With this much data, the evidence dominates and the prior barely matters.':st.n<=3?'With so little data, your prior still carries real weight.':'Prior and data each pull their share; add data and the posterior tightens toward the evidence.');el.querySelector('.bu-v-pm').textContent=st.pm.toFixed(1);el.querySelector('.bu-v-ps').textContent=st.ps.toFixed(1);el.querySelector('.bu-v-dm').textContent=st.dm.toFixed(1);el.querySelector('.bu-v-n').textContent=st.n;} Array.prototype.forEach.call(el.querySelectorAll('.bu-s'),function(s){s.addEventListener('input',function(){st[s.getAttribute('data-k')]=+s.value;draw();});});draw();} function rcode(){return['# Conjugate normal-normal update: prior belief x data evidence -> posterior.','prior_mean <- 0; prior_sd <- 1 # what you believed before','data_mean <- 3; data_sd <- 2; n <- 10 # what the data say','','prior_var <- prior_sd^2','data_var <- data_sd^2 / n # the mean of n points is this precise','post_var <- 1 / (1/prior_var + 1/data_var)','post_mean <- post_var * (prior_mean/prior_var + data_mean/data_var)','c(post_mean = round(post_mean, 3), post_sd = round(sqrt(post_var), 3))','# posterior mean is a precision-weighted blend of prior and data;','# more data (raise n) shrinks data_var, so the posterior follows the data.'].join('\n');} window.LessonWidgets.register('bayes-update',mount);})();;(function(){'use strict';var u=window.LessonWidgets.u,P=u.P;function f(x){return 1.5*Math.exp(-(x-6)*(x-6)/1.5)+Math.exp(-(x-2.5)*(x-2.5)/0.8);} function rbf(a,b,l){var d=a-b;return Math.exp(-d*d/(2*l*l));} function pnorm(z){var b1=0.319381530,b2=-0.356563782,b3=1.781477937,b4=-1.821255978,b5=1.330274429,p=0.2316419,c=0.39894228;var t=1/(1+p*Math.abs(z)),y=1-c*Math.exp(-z*z/2)*t*(b1+t*(b2+t*(b3+t*(b4+t*b5))));return z>=0?y:1-y;} function dnorm(z){return 0.39894228*Math.exp(-z*z/2);} function inv(M){var n=M.length,A=M.map(function(r,i){return r.concat(Array.from({length:n},function(_,j){return i===j?1:0;}));});for(var c=0;cNext sample →'+''+''+'
'+'
'+ u.runnable(rcode(),{label:'The same Expected-Improvement loop in base R'});var plot=el.querySelector('.bo-plot'),read=el.querySelector('.bo-read');var W=340,Ht=170,Ha=70,RX=[0,8],RYo=[-0.2,1.8];function px(x){return(x-RX[0])/(RX[1]-RX[0])*W;} function pyo(y){return Ht-(y-RYo[0])/(RYo[1]-RYo[0])*Ht;} function draw(nextX){var post=surrogate(X,Y);var maxEI=Math.max.apply(null,post.map(function(p){return p.ei;})),argEI=post.reduce(function(a,b){return b.ei>a.ei?b:a;}).x;var truePts=GRID.map(function(x){return px(x).toFixed(1)+','+pyo(f(x)).toFixed(1);});var meanPts=post.map(function(p){return px(p.x).toFixed(1)+','+pyo(p.m).toFixed(1);});var top=post.map(function(p){return[px(p.x),pyo(p.m+1.96*p.sd)];}),bot=post.map(function(p){return[px(p.x),pyo(p.m-1.96*p.sd)];});var band='M'+top.map(function(p){return p[0].toFixed(1)+','+p[1].toFixed(1);}).join(' L')+' L'+bot.reverse().map(function(p){return p[0].toFixed(1)+','+p[1].toFixed(1);}).join(' L')+' Z';var dots=X.map(function(xt,i){return'';}).join('');var objSvg=''+''+''+''+dots+ (nextX!=null?'':'')+'';var eiPts=post.map(function(p){return px(p.x).toFixed(1)+','+(Ha-p.ei/(maxEI||1)*(Ha-8)).toFixed(1);});var acqSvg=''+''+''+'acquisition (EI)';plot.innerHTML=objSvg+acqSvg;var bi=Y.indexOf(Math.max.apply(null,Y));read.innerHTML='Evaluations: '+X.length+'. Best so far: f('+X[bi].toFixed(2)+') = '+Y[bi].toFixed(2)+' (true max ≈ 1.50 at x≈6). The blue curve is Expected Improvement; its peak is where BO looks next, balancing a high predicted mean against wide uncertainty.';} el.querySelector('.bo-next').addEventListener('click',function(){var post=surrogate(X,Y),pick=post.reduce(function(a,b){return b.ei>a.ei?b:a;}).x;X.push(pick);Y.push(f(pick));draw();});el.querySelector('.bo-reset').addEventListener('click',function(){reset();draw();});(function(){var post=surrogate(X,Y);draw(post.reduce(function(a,b){return b.ei>a.ei?b:a;}).x);})();} function rcode(){return['# Bayesian optimization: a GP surrogate + Expected Improvement, base R.','f <- function(x) 1.5*exp(-(x-6)^2/1.5) + exp(-(x-2.5)^2/0.8) # expensive black box','rbf <- function(a,b,l) exp(-outer(a,b,"-")^2/(2*l^2))','xg <- seq(0, 8, length.out = 100)','X <- c(1, 4, 7.5); Y <- f(X) # 3 initial evaluations','ei_pick <- function(X, Y, l=0.9, s0=1e-4) {',' K <- rbf(X,X,l) + s0*diag(length(X)); Ki <- solve(K)',' Ks <- rbf(xg, X, l)',' mu <- as.numeric(Ks %*% (Ki %*% Y)) # surrogate mean',' s <- sqrt(pmax(1 - rowSums((Ks %*% Ki) * Ks), 1e-9)) # surrogate sd',' z <- (mu - max(Y)) / s',' ei <- (mu - max(Y))*pnorm(z) + s*dnorm(z) # expected improvement',' xg[which.max(ei)] # look here next','}','for (i in 1:5) { xn <- ei_pick(X, Y); X <- c(X, xn); Y <- c(Y, f(xn)) }','round(c(best_x = X[which.max(Y)], best_y = max(Y)), 2)','# a handful of evaluations find the global peak (x=6, f=1.5).'].join('\n');} window.LessonWidgets.register('bayesopt-acq',mount);})();;(function(){'use strict';var u=window.LessonWidgets.u,P=u.P;var ZX=[0.5,-0.8,0.2,1.0,-0.4,0.7,-1.1,0.3,-0.2,0.9,-0.6,0.1];var ZY=[-0.6,0.4,1.0,-0.3,-0.9,0.5,0.2,-1.0,0.8,-0.2,0.6,-0.5];function mount(el,cfg){cfg=cfg||{};var bias=(cfg.bias!=null)?+cfg.bias:1.2,variance=(cfg.variance!=null)?+cfg.variance:0.4;el.style.cssText='border:1px solid '+P.line+';border-radius:12px;background:#fff;padding:16px 17px';el.innerHTML='
'+'
'+''+''+'
'+'
'+ u.runnable(rcode(),{label:'Simulate the shots + split the error in R'});var chart=el.querySelector('.bt-chart'),read=el.querySelector('.bt-read'),bs=el.querySelector('.bt-bs'),vs=el.querySelector('.bt-vs'),bEl=el.querySelector('.bt-b'),vEl=el.querySelector('.bt-v');function draw(){var S=280,cx=S/2,cy=S/2,unit=S/7,sd=Math.sqrt(variance);function px(x){return cx+x*unit;}function py(y){return cy-y*unit;} var sxs=ZX.map(function(z){return bias+z*sd;}),sys=ZY.map(function(z){return z*sd;});var svg='';[2.4,1.6,0.8].forEach(function(rr,i){svg+='';});svg+='';var mcx=sxs.reduce(function(a,b){return a+b;},0)/ZX.length,mcy=sys.reduce(function(a,b){return a+b;},0)/ZY.length;svg+='';sxs.forEach(function(x,i){svg+='';});svg+='';svg+='';chart.innerHTML=svg;var bias2=mcx*mcx+mcy*mcy,varEst=sxs.reduce(function(a,x,i){return a+(x-mcx)*(x-mcx)+(sys[i]-mcy)*(sys[i]-mcy);},0)/ZX.length;var label=bias>1&&variance>0.7?'High bias AND high variance - off-center and scattered.':bias>1?'High bias, low variance - tightly grouped, but in the wrong spot (a too-simple model).':variance>0.7?'Low bias, high variance - centered on average, but wildly inconsistent (overfitting).':'Low bias, low variance - the goal: tight and on target.';read.innerHTML=''+label+' Error splits cleanly: bias² '+bias2.toFixed(2)+' + variance '+varEst.toFixed(2)+' = MSE '+(bias2+varEst).toFixed(2)+'. The dashed line is how far the cluster center sits from the bullseye (bias); the scatter around that center is variance.';bEl.textContent=(+bias).toFixed(2);vEl.textContent=(+variance).toFixed(2);} bs.addEventListener('input',function(){bias=+bs.value;draw();});vs.addEventListener('input',function(){variance=+vs.value;draw();});draw();} function rcode(){return['# Bias vs variance as darts at a bullseye. Bias shifts the cluster CENTER','# off target; variance is its SPREAD. Total error = bias^2 + variance.','set.seed(1)','bias <- 1.2; variance <- 0.4; n <- 12 # try bias 0 + variance 1.0, etc.','shots_x <- bias + rnorm(n, 0, sqrt(variance))','shots_y <- rnorm(n, 0, sqrt(variance))','','plot(shots_x, shots_y, xlim = c(-3, 3), ylim = c(-3, 3), asp = 1,',' pch = 19, col = "firebrick", xlab = "", ylab = "")','symbols(rep(0, 3), rep(0, 3), circles = c(0.6, 1.4, 2.2), inches = FALSE, add = TRUE)','','center <- c(mean(shots_x), mean(shots_y))','bias2 <- sum(center^2) # distance from bullseye','var_ <- mean((shots_x - center[1])^2 + (shots_y - center[2])^2)','c(bias2 = bias2, variance = var_, mse = bias2 + var_)'].join('\n');} window.LessonWidgets.register('bias-variance-target',mount);})();;(function(){'use strict';var u=window.LessonWidgets.u,P=u.P;function mount(el,cfg){cfg=cfg||{};var MAX=Math.max(4,+cfg.maxDegree||12);var deg=Math.min(MAX,Math.max(1,+cfg.start||3));function errs(d){var noise=0.18;var test=noise+1.55/d+0.05*d;var train=noise*0.6+1.35/(d+1.1)-0.025*d;return{train:Math.max(0.04,train),test:test};} var DS=[];for(var d=1;d<=MAX;d++){var e=errs(d);DS.push({d:d,train:e.train,test:e.test});} var bestD=DS.reduce(function(b,r){return r.testModel complexity (polynomial degree): '+''+deg+''+''+''+''+'
'+'
'+ u.runnable(rcode(),{label:'Fit it for real: the bias-variance U-curve in R'});var chart=el.querySelector('.bv-chart'),read=el.querySelector('.bv-read'),tag=el.querySelector('.bv-tag'),degEl=el.querySelector('.bv-deg'),slider=el.querySelector('.bv-slider');function draw(){var W=480,H=250,m={t:14,r:14,b:36,l:44},iw=W-m.l-m.r,ih=H-m.t-m.b;var maxY=Math.max.apply(null,DS.map(function(r){return r.test;}))*1.08;function sx(dd){return m.l+(dd-1)/(MAX-1)*iw;} function sy(v){return m.t+ih-v/maxY*ih;} function poly(key,col){return'';} var cur=DS[deg-1];var svg='';svg+='';svg+='';svg+='';svg+='sweet spot';svg+=poly('test',P.c1)+poly('train',P.acc);svg+='';svg+='';svg+='';svg+='model complexity →';svg+='prediction error';svg+='test';svg+='train';svg+='';chart.innerHTML=svg;var state=deg<=Math.max(1,bestD-2)?['Underfitting',P.c0,'#e8f0f9','Too simple: high bias. The model misses the real pattern, so both train and test error are high.']:deg>=bestD+2?['Overfitting',P.bad,P.del,'Too flexible: high variance. Train error keeps falling, but the model is memorizing noise and test error climbs.']:['About right',P.acc,P.add,'Near the sweet spot: enough flexibility to fit the signal, not so much that it chases noise. Test error is at its lowest.'];tag.textContent=state[0];tag.style.color=state[1];tag.style.background=state[2];read.innerHTML=state[3]+' train '+cur.train.toFixed(2)+' · test '+cur.test.toFixed(2)+'';degEl.textContent=deg;} slider.addEventListener('input',function(){deg=+slider.value;draw();});draw();} function rcode(){return['# The bias-variance tradeoff: fit polynomials of rising degree to noisy data,','# then watch train error fall while test error traces a U.','set.seed(1)','n <- 120','x <- sort(runif(n, -3, 3))','y <- sin(x) + rnorm(n, 0, 0.35) # true signal + noise','train <- data.frame(x = x[1:80], y = y[1:80])','test <- data.frame(x = x[81:n], y = y[81:n])','','deg <- 1:12','err <- sapply(deg, function(d) {',' fit <- lm(y ~ poly(x, d), data = train)',' c(train = sqrt(mean((train$y - predict(fit))^2)),',' test = sqrt(mean((test$y - predict(fit, test))^2)))','})','','plot(deg, err["test", ], type = "b", col = "darkorange", lwd = 2,',' ylim = range(err), xlab = "polynomial degree (complexity)", ylab = "RMSE")','lines(deg, err["train", ], type = "b", col = "forestgreen", lwd = 2)','legend("topright", c("test error", "train error"),',' col = c("darkorange", "forestgreen"), lwd = 2, bty = "n")'].join('\n');} window.LessonWidgets.register('bias-variance',mount);})();;(function(){'use strict';function mulberry32(a){return function(){a|=0;a=a+0x6D2B79F5|0;var t=Math.imul(a^a>>>15,1|a);t=t+Math.imul(t^t>>>7,61|t)^t;return((t^t>>>14)>>>0)/4294967296;};} var C_IN='#1f7a55',C_DUP='#2563a8',C_OOB='#aeb6c2',INK='#131720',MUT='#677084';function mount(el,cfg){cfg=cfg||{};var n=cfg.n||12,seed0=cfg.seed||7,draw=0;var tail=cfg.tail||'Those rows trained no tree here, so they are a free test set.';el.innerHTML='
'+'
'+'
'+''+'
'+' in the sample   picked more than once   out-of-bag
'+'
';var strip=el.querySelector('.lw-bs-strip'),note=el.querySelector('.lw-bs-note'),btn=el.querySelector('.lw-bs-draw'),i;function render(){var rng=mulberry32(seed0+draw*977),counts=[];for(i=0;i1?C_DUP:C_IN);if(c===0)oob++;var lbl=c===0?'OOB':('#'+(i+1)+(c>1?(' x'+c):''));html+=''+lbl+'';} strip.innerHTML=html;var pct=Math.round(oob/n*100);note.innerHTML='Out-of-bag this draw: '+oob+' of '+n+' rows ('+pct+'%). '+tail;} btn.addEventListener('click',function(){draw++;render();});render();} if(window.LessonWidgets)window.LessonWidgets.register('bootstrap-sample',mount);})();;(function(){'use strict';var u=window.LessonWidgets.u,P=u.P;function mount(el,cfg){cfg=cfg||{};var skew=1;var BINS=[0.1,0.3,0.5,0.7,0.9];el.style.cssText='border:1px solid '+P.line+';border-radius:12px;background:#fff;padding:16px 17px';el.innerHTML='
'+''+'
'+ u.runnable(rcode(),{label:'Bin probabilities and draw the reliability curve in R'});var plot=el.querySelector('.cc-plot'),read=el.querySelector('.cc-read'),slider=el.querySelector('.cc-s'),lEl=el.querySelector('.cc-l');function observed(p){var o=Math.pow(p,skew)/(Math.pow(p,skew)+Math.pow(1-p,skew));return o;} function draw(){var S=240,m=34,iw=S-m-12,ih=S-m-12;function px(x){return m+x*iw;}function py(y){return(S-m)-y*ih;} var pts=BINS.map(function(p){return[p,observed(p)];});var line=pts.map(function(q){return px(q[0]).toFixed(1)+','+py(q[1]).toFixed(1);}).join(' ');var svg='';svg+='';svg+='';svg+='';pts.forEach(function(q){svg+='';});svg+='mean predicted probability';svg+='observed frequency';svg+='';plot.innerHTML=svg;var state=Math.abs(skew-1)<0.06?'well calibrated - points sit on the diagonal, so a predicted 0.7 really does happen about 70% of the time':(skew>1?'over-confident - the curve bows below the diagonal, so high predictions happen less often than claimed':'under-confident - the curve bows above the diagonal, so the model hedges when it could be surer');read.innerHTML='This model is '+state+'. Calibration (Platt or isotonic) bends the curve back onto the diagonal.';lEl.textContent=skew>1.06?'over-confident':(skew<0.94?'under-confident':'calibrated');} slider.addEventListener('input',function(){skew=+slider.value;draw();});draw();} function rcode(){return['# A reliability diagram bins predictions, then asks: of the cases I called ~0.7,','# what fraction were actually positive? Calibrated => that fraction is ~0.7.','set.seed(1)','n <- 600; x <- rnorm(n)','p_true <- plogis(1.2 * x) # the real probability','y <- rbinom(n, 1, p_true)','fit <- glm(y ~ x, family = binomial)','p_hat <- predict(fit, type = "response")','','bins <- cut(p_hat, breaks = seq(0, 1, by = 0.1), include.lowest = TRUE)','cal <- data.frame(',' predicted = tapply(p_hat, bins, mean),',' observed = tapply(y, bins, mean))','cal <- cal[complete.cases(cal), ]','','plot(cal$predicted, cal$observed, type = "b", pch = 19,',' xlim = 0:1, ylim = 0:1, xlab = "predicted", ylab = "observed")','abline(0, 1, lty = 2) # perfect calibration','cal'].join('\n');} window.LessonWidgets.register('calibration-curve',mount);})();;(function(){'use strict';var u=window.LessonWidgets.u,P=u.P;var SCEN={confounder:{arrows:[['Z','X'],['Z','Y'],['X','Y']],say:'Z is a confounder (it causes both X and Y). Leaving it out makes the X->Y link look stronger or weaker than it is. Control for Z.'},collider:{arrows:[['X','Z'],['Y','Z'],['X','Y']],say:'Z is a collider (both X and Y point into it). Controlling for it OPENS a fake path and invents a correlation. Do not control for Z.'},mediator:{arrows:[['X','Z'],['Z','Y']],say:'Z is a mediator (X acts through Z). Control for it and you remove the very effect you wanted to measure. Usually leave it free.'}};var POS={X:[70,150],Y:[330,150],Z:[200,50]};function mount(el,cfg){cfg=cfg||{};var mode='confounder';el.style.cssText='border:1px solid '+P.line+';border-radius:12px;background:#fff;padding:16px 17px';el.innerHTML='
'+'
'+'
'+ u.runnable(rcode(),{label:'Watch a confounder flip a coefficient in R'});var seg=el.querySelector('.cd-seg'),plot=el.querySelector('.cd-plot'),read=el.querySelector('.cd-read');[['confounder','confounder'],['collider','collider'],['mediator','mediator']].forEach(function(o){var b=document.createElement('button');b.textContent=o[1];b.style.cssText='font:600 12px IBM Plex Sans,sans-serif;border:1px solid '+P.line+';border-radius:8px;padding:6px 12px;cursor:pointer';b.addEventListener('click',function(){mode=o[0];draw();});seg.appendChild(b);});function node(n,lab){var p=POS[n];var hi=(mode==='confounder'&&n==='Z')||(mode==='collider'&&n==='Z');return''+lab+'';} function arrow(a,b){var pa=POS[a],pb=POS[b],dx=pb[0]-pa[0],dy=pb[1]-pa[1],L=Math.sqrt(dx*dx+dy*dy),ux=dx/L,uy=dy/L;var x1=pa[0]+ux*27,y1=pa[1]+uy*27,x2=pb[0]-ux*30,y2=pb[1]-uy*30;var ang=Math.atan2(uy,ux);return''+'';} function draw(){seg.querySelectorAll('button').forEach(function(x,i){var on=['confounder','collider','mediator'][i]===mode;x.style.background=on?P.ink:'#fff';x.style.color=on?'#fff':P.body;});var sc=SCEN[mode],svg='';sc.arrows.forEach(function(a){svg+=arrow(a[0],a[1]);});svg+=node('X','X')+node('Y','Y')+node('Z','Z');svg+='treatmentoutcome';svg+='';plot.innerHTML=svg;read.innerHTML=sc.say;} draw();} function rcode(){return['# A confounder Z drives both X and Y. Ignore it and the X->Y coefficient lies.','set.seed(1); n <- 500','Z <- rnorm(n)','X <- 0.8 * Z + rnorm(n) # Z causes X','Y <- 1.0 * Z + rnorm(n) # Z causes Y; X has NO real effect on Y','','coef(lm(Y ~ X))["X"] # biased: looks like X affects Y','coef(lm(Y ~ X + Z))["X"] # adjust for Z: the effect collapses to ~0'].join('\n');} window.LessonWidgets.register('causal-dag',mount);})();;(function(){'use strict';var GEOML={point:'geom_point()',line:'geom_line()',bar:'geom_col()',col:'geom_col()',histogram:'geom_histogram(bins = 10)',boxplot:'geom_boxplot()'};function mount(el,cfg){var u=window.LessonWidgets.u;if(!u)return;cfg=cfg||{};var data=cfg.data||[{x:1,y:12},{x:2,y:19},{x:3,y:15},{x:4,y:25},{x:5,y:22},{x:6,y:30}];var geoms=cfg.geoms||['point','line','bar'];var xlab=cfg.x||'month',ylab=cfg.y||'sales',codeMap=cfg.code||{};var cur=geoms[0];var has=data[0]||{};function ggline(g){if(codeMap[g])return codeMap[g];var aesX=(g==='bar'||g==='col')?'factor('+xlab+')':xlab;if(g==='histogram')return'ggplot(df, aes('+xlab+')) +\n '+GEOML[g];if(g==='boxplot')return'ggplot(df, aes('+xlab+', '+ylab+')) +\n '+GEOML[g];return'ggplot(df, aes('+aesX+', '+ylab+')) +\n '+(GEOML[g]||'geom_point()');} function runnableCode(g){var cols=[{name:xlab,key:'x'}];if('y'in has)cols.push({name:ylab,key:'y'});if('fill'in has)cols.push({name:'group',key:'fill'});var line=ggline(g);var mm=line.match(/\bggplot\s*\(\s*([A-Za-z.][\w.]*)/);var frame=mm?mm[1]:'df';return'library(ggplot2)\n'+u.rdf(data,cols,frame)+'\n\n'+line;} var wrap=document.createElement('div');wrap.style.cssText='font-family:IBM Plex Sans,system-ui,sans-serif';wrap.innerHTML='
'+'
';var segHost=wrap.querySelector('.cp-seg');segHost.innerHTML=u.seg(geoms.map(function(g){return{v:g,label:g};}),cur);var codeEl=wrap.querySelector('.cp-code');function render(g){codeEl.innerHTML=u.runnable(runnableCode(g),{label:'Run this chart'});var po=codeEl.querySelector('.webr-plot-output');if(po){po.innerHTML=u.previewSeed(u.plot(data,{geom:g,x:xlab,y:ylab,corr:g==='point'}));po.classList.add('has-content');}} u.wireSeg(segHost,function(v){cur=v;render(v);});render(cur);el.innerHTML='';el.appendChild(wrap);} if(window.LessonWidgets)window.LessonWidgets.register('chart-plotter',mount);})();;(function(){'use strict';var u=window.LessonWidgets.u;var LIGHT={ink:'#131720',body:'#434b59',mut:'#677084',faint:'#97a0b2',line:'#d8dee9',grid:'#eef1f6',acc:'#1f7a55',c0:'#2563a8',c1:'#b5631a',bad:'#c2410c',panel:'#ffffff',soft:'#f3f6f4'};var DARK={ink:'#eef4fb',body:'#c3d1e3',mut:'#93a4bb',faint:'#6f8299',line:'rgba(255,255,255,.24)',grid:'rgba(255,255,255,.10)',acc:'#46c08a',c0:'#7fb2ea',c1:'#e3a05a',bad:'#f4805a',panel:'#101c2b',soft:'rgba(255,255,255,.05)'};function lumOf(c){c=String(c).trim();var r,g,b,m;if(c.charAt(0)==='#'){if(c.length===4){r=parseInt(c[1]+c[1],16);g=parseInt(c[2]+c[2],16);b=parseInt(c[3]+c[3],16);} else{r=parseInt(c.substr(1,2),16);g=parseInt(c.substr(3,2),16);b=parseInt(c.substr(5,2),16);}}else if((m=c.match(/rgba?\(([^)]+)\)/))){var p=m[1].split(',');r=+p[0];g=+p[1];b=+p[2];} else return 1;return(0.2126*r+0.7152*g+0.0722*b)/255;} function isDark(el){try{var v=getComputedStyle(el).getPropertyValue('--lm-panel');if(v&&v.trim())return lumOf(v)<0.45;}catch(e){} return!!(document.documentElement&&document.documentElement.classList.contains('dark'));} function palette(el){return isDark(el)?DARK:LIGHT;} function mulberry32(a){return function(){a|=0;a=a+0x6D2B79F5|0;var t=Math.imul(a^a>>>15,1|a);t=t+Math.imul(t^t>>>7,61|t)^t;return((t^t>>>14)>>>0)/4294967296;};} function gaussian(rnd){var a=1-rnd(),b=rnd();return Math.sqrt(-2*Math.log(a))*Math.cos(2*Math.PI*b);} function lgamma(x){var c=[76.18009172947146,-86.50532032941677,24.01409824083091,-1.231739572450155,0.1208650973866179e-2,-0.5395239384953e-5];var y=x,t=x+5.5,s=1.000000000190015;t-=(x+0.5)*Math.log(t);for(var j=0;j<6;j++)s+=c[j]/++y;return-t+Math.log(2.5066282746310005*s/x);} function betacf(a,b,x){var FPMIN=1e-300,qab=a+b,qap=a+1,qam=a-1,c=1,d=1-qab*x/qap;if(Math.abs(d)=1)return 1;var bt=Math.exp(lgamma(a+b)-lgamma(a)-lgamma(b)+a*Math.log(x)+b*Math.log(1-x));return(x<(a+1)/(a+b+2))?bt*betacf(a,b,x)/a:1-bt*betacf(b,a,1-x)/b;} function pt2(t,df){return ibeta(df/2,0.5,df/(df+t*t));} function qt(p,df){var lo=0,hi=60,i;for(i=0;i<140;i++){var mid=(lo+hi)/2;if(1-pt2(mid,df)/2'+''+'
'+'
'+'
'+'
';var lab=el.querySelector('.ci-lab'),plot=el.querySelector('.ci-plot'),tbl=el.querySelector('.ci-tbl'),read=el.querySelector('.ci-read'),slider=el.querySelector('.ci-rho'),rbox=el.querySelector('.ci-r');function render(first){P=palette(el);el.style.background=P.panel;el.style.borderColor=P.line;lab.style.color=P.mut;read.style.color=P.body;var S=sim(step),rho=S.rho;build(rho,yShow,uFix,eFix);var one=analyse(yShow,k,m,arm);lab.innerHTML='Intraclass correlation: '+rho.toFixed(2)+' '+'('+k+' clusters of '+m+', treatment assigned to whole clusters)';plot.innerHTML=draw(yShow,one,S,k,m,arm,tau,P);tbl.innerHTML=table(one,S,tau,P);read.innerHTML=verdict(S,one,k,m,P);if(first)rbox.innerHTML=u.runnable(rcode(k,m,tau),{label:'Run the same comparison in R'});} slider.addEventListener('input',function(){step=+slider.value;render(false);});var wasDark=isDark(el);if(window.MutationObserver){var mo=new MutationObserver(function(){if(!document.contains(el)){mo.disconnect();return;} var now=isDark(el);if(now!==wasDark){wasDark=now;render(false);}});mo.observe(document.documentElement,{attributes:true,attributeFilter:['class']});} render(true);} function draw(y,one,S,k,m,arm,tau,P){var W=520,mg={l:40,r:14,t:30},hS=138,n=k*m;var g='',i,j;var lo=-3.2,hi=3.2+tau;function sy(v){return mg.t+(hi-Math.max(lo,Math.min(hi,v)))/(hi-lo)*hS;} var colW=(W-mg.l-mg.r)/k;g+='One trial: every dot is an observation, every column a cluster';[-2,0,2].forEach(function(v){g+=''+''+v+'';});for(j=0;j';} cm/=m;g+='';} g+=''+'control clusters'+'treated clusters';var yB=mg.t+hS+42,rowH=21,barL=186;var vals=METHODS.map(function(M){return one[M.key];});var maxV=Math.max(S.sd,Math.max.apply(null,vals))*1.16;function bw(v){return(v/maxV)*(W-barL-22);} g+='Standard error for that one estimate, four ways';METHODS.forEach(function(M,r){var yy=yB+r*rowH,v=one[M.key],wd=bw(v);var off=vW-46;g+=''+u.esc(M.label)+''+''+''+v.toFixed(3)+'';});var xT=barL+bw(S.sd);var capAnchor=xT>W-108?'end':(xT<108?'start':'middle');var capX=capAnchor==='end'?W-2:(capAnchor==='start'?2:xT);g+=''+'how much the estimate really varies';var H=yB+METHODS.length*rowH+8;return''+g+'';} function table(one,S,tau,P){var cols=['Analysis','SE','95% interval','p','Covers truth in'];var rows=METHODS.map(function(M,r){var se=one[M.key],df=one[M.df],tc=qt(0.975,df),t=one.est/se;return[M.label,se.toFixed(3),'['+(one.est-tc*se).toFixed(2)+', '+(one.est+tc*se).toFixed(2)+']',fmtP(pt2(Math.abs(t),df)),(100*S.cov[r]).toFixed(1)+'%'];});var h='';cols.forEach(function(c,ci){h+='';});h+='';rows.forEach(function(r,ri){var bad=S.cov[ri]<0.9;h+='';r.forEach(function(v,ci){h+='';});h+='';});return h+'
'+u.esc(c)+'
'+u.esc(v)+'
'+'
'+'All four rows describe the same estimate on the same data. The true effect is '+tau+'.
';} function fmtP(p){return p<0.001?'<0.001':p.toFixed(3);} function verdict(S,one,k,m,P){var rho=S.rho,deff=S.deff,nEff=(k*m)/deff;if(rho<1e-9){return'At an intraclass correlation of zero the four standard errors agree, because there is nothing to correct for. '+'Raise it and watch which ones move.';} var ratio=one.seCM/one.seNaive;return'The design effect is 1 + ('+m+' − 1) × '+rho.toFixed(2)+' = '+deff.toFixed(2)+', '+'so '+(k*m)+' rows carry about '+nEff.toFixed(0)+' rows of independent information. '+'The naive interval covers the truth '+(100*S.cov[0]).toFixed(1)+'% of the time instead of 95%. '+'HC1 gets to '+(100*S.cov[1]).toFixed(1)+'%: it is robust to the wrong thing. '+'Cluster-robust reaches '+(100*S.cov[2]).toFixed(1)+'% and the cluster-level analysis '+(100*S.cov[3]).toFixed(1)+'%. '+'The simulated spread of the estimate is '+S.sd.toFixed(3)+' against the formula\'s '+S.trueSd.toFixed(3)+'.';} function rcode(k,m,tau){return['# One cluster-randomised trial, analysed four ways. Raise rho and run it again.','set.seed(4)','k <- '+k+'; m <- '+m+'; n <- k * m; tau <- '+tau,'rho <- 0.30 # intraclass correlation','','cl <- rep(1:k, each = m)','arm <- rep(rep(0:1, length.out = k), each = m) # treatment is a CLUSTER property','y <- tau * arm + sqrt(rho) * rnorm(k)[cl] + sqrt(1 - rho) * rnorm(n)','','fit <- lm(y ~ arm)','e <- residuals(fit)','nT <- sum(arm == 1); nC <- n - nT','','se_ols <- coef(summary(fit))["arm", "Std. Error"]','se_hc1 <- sqrt(n / (n - 2) * (sum(e[arm == 1]^2) / nT^2 + sum(e[arm == 0]^2) / nC^2))','g <- tapply(e, cl, sum)','armcl <- tapply(arm, cl, max)','se_cr1 <- sqrt(k / (k - 1) * (n - 1) / (n - 2) *',' (sum(g[armcl == 1]^2) / nT^2 + sum(g[armcl == 0]^2) / nC^2))','','# cluster means: for a balanced design with cluster-level treatment this is exactly','# the standard error a random-intercept model reports','cm <- tapply(y, cl, mean)','se_cm <- sqrt(sum((cm - ave(cm, armcl))^2) / (k - 2) * (2 / (k / 2)))','','round(c(ols = se_ols, hc1 = se_hc1, cluster_robust = se_cr1, cluster_means = se_cm), 4)','round(c(design_effect = 1 + (m - 1) * rho,',' true_se = sqrt(4 * (rho + (1 - rho) / m) / k)), 4)'].join('\n');} window.LessonWidgets.register('cluster-icc-sim',mount);})();;(function(){'use strict';var u=window.LessonWidgets.u,P=u.P;var WSS=[null,520,250,120,96,82,73,66,60];var SIL=[null,0,0.42,0.61,0.55,0.47,0.40,0.34,0.29];function mount(el,cfg){cfg=cfg||{};var k=3,view='elbow';el.style.cssText='border:1px solid '+P.line+';border-radius:12px;background:#fff;padding:16px 17px';el.innerHTML='
'+'
'+''+'
'+ u.runnable(rcode(),{label:'Compute the within-SS elbow in R'});var seg=el.querySelector('.cvd-seg'),plot=el.querySelector('.cvd-plot'),read=el.querySelector('.cvd-read'),slider=el.querySelector('.cvd-s'),kEl=el.querySelector('.cvd-k');[['elbow','elbow (within-SS)'],['sil','silhouette']].forEach(function(o){var b=document.createElement('button');b.textContent=o[1];b.style.cssText='font:600 12px IBM Plex Sans,sans-serif;border:1px solid '+P.line+';border-radius:8px;padding:6px 12px;cursor:pointer';b.addEventListener('click',function(){view=o[0];draw();});seg.appendChild(b);});function draw(){seg.querySelectorAll('button').forEach(function(x,i){var on=['elbow','sil'][i]===view;x.style.background=on?P.ink:'#fff';x.style.color=on?'#fff':P.body;});var W=420,H=170,m=32,iw=W-m-14,ih=H-m-8;function px(kk){return m+(kk-2)/6*iw;}function pyE(v){return(H-m)-(v-55)/(520-55)*ih;}function pyS(v){return(H-m)-v/0.7*ih;} var svg='';svg+='';if(view==='elbow'){var line=[];for(var kk=2;kk<=8;kk++)line.push(px(kk).toFixed(1)+','+pyE(WSS[kk]).toFixed(1));svg+='';for(var kk2=2;kk2<=8;kk2++)svg+='';}else{for(var kk3=2;kk3<=8;kk3++){var h=SIL[kk3]/0.7*ih;svg+='';}} for(var t=2;t<=8;t++)svg+=''+t+'';svg+='';plot.innerHTML=svg;read.innerHTML='At k='+k+': within-SS '+WSS[k]+', silhouette '+SIL[k].toFixed(2)+'. The elbow bends and the silhouette peaks around k=3 - the natural number of groups here.';kEl.textContent=k;} slider.addEventListener('input',function(){k=+slider.value;draw();});draw();} function rcode(){return['# The "elbow": total within-cluster spread falls fast, then flattens. The bend = good k.','x <- scale(iris[, 1:4])','wss <- sapply(1:8, function(k) kmeans(x, centers = k, nstart = 10)$tot.withinss)','plot(1:8, wss, type = "b", xlab = "k", ylab = "within-cluster SS")','wss # the sharp drop ends around k = 3'].join('\n');} window.LessonWidgets.register('cluster-validate',mount);})();;(function(){'use strict';var u=window.LessonWidgets.u,P=u.P;var NAMES=['x1','x2','x3','x4','x5','x6'];var FULL=[2.4,-1.8,1.1,0.6,-0.35,0.15];var STEPS=40;function paths(mode){var out=[];for(var s=0;s'+'
'+''+'
'+ u.runnable(rcode(),{label:'The lasso path with glmnet'});var plot=el.querySelector('.cp-plot'),read=el.querySelector('.cp-read'),slider=el.querySelector('.cp-s'),lamEl=el.querySelector('.cp-lam');var W=440,H=220,m={l:40,r:54,t:12,b:28},iw=W-m.l-m.r,ih=H-m.t-m.b;var yb=2.8;function sx(s){return m.l+s/(STEPS-1)*iw;} function sy(v){return m.t+ih/2-v/yb*(ih/2);} function draw(){var P2=paths(mode),pal=[P.c0,P.acc,P.c1,'#7a5ea3','#2f8f86',P.faint];var svg='';svg+='';NAMES.forEach(function(nm,j){var pts='';for(var s=0;s';svg+=''+nm+'';});svg+='';svg+='weak penaltystrong penalty';svg+='';plot.innerHTML=svg;var alive=P2[li].filter(function(v){return Math.abs(v)>0.01;}).length;lamEl.textContent=(li/(STEPS-1)).toFixed(2);read.innerHTML=mode==='lasso'?''+alive+' of 6 features survive. Lasso drives weak coefficients to exactly zero, so raising λ performs feature selection: the noise features (x5, x6) die first.':'all 6 shrink, none vanish. Ridge pulls every coefficient smoothly toward zero but never exactly to it, so it stabilizes rather than selects.';} u.wireSeg(el.querySelector('.cp-seg'),function(v){mode=v;draw();});slider.addEventListener('input',function(){li=+slider.value;draw();});draw();} function rcode(){return['# The lasso path: coefficients shrink and drop to zero as the penalty grows.','library(glmnet)','set.seed(1); n <- 200','X <- matrix(rnorm(n * 6), n, 6)','y <- 2.4*X[,1] - 1.8*X[,2] + 1.1*X[,3] + rnorm(n) # x4-x6 are noise','','fit <- glmnet(X, y, alpha = 1) # alpha = 1 is lasso; alpha = 0 is ridge','coef(fit, s = 0.30) # at this lambda, weak features are exactly 0','colSums(coef(fit) != 0) # non-zero count shrinks as lambda rises'].join('\n');} window.LessonWidgets.register('coef-path',mount);})();;(function(){'use strict';var u=window.LessonWidgets.u,P=u.P;var H=0.08,pRel=0.6,pDeath=0.4;function surv(t){return Math.exp(-H*t);} function cifRel(t){return pRel*(1-surv(t));} function cifDeath(t){return pDeath*(1-surv(t));} function mount(el,cfg){cfg=cfg||{};var mark=10;el.style.cssText='border:1px solid '+P.line+';border-radius:12px;background:#fff;padding:16px 17px';el.innerHTML='
'+u.seg([{v:'5',label:'t = 5'},{v:'10',label:'t = 10'},{v:'20',label:'t = 20'}],'10')+'
'+'
'+'
'+ u.runnable(rcode(),{label:'Cumulative incidence for competing events with survival::survfit'});var plot=el.querySelector('.cr-plot'),read=el.querySelector('.cr-read');var W=340,H2=240,TMAX=30;function px(t){return(t/TMAX)*(W-8)+4;} function py(v){return H2-v*(H2-14)-7;} function band(lo,hi,col,op){var N=80,top=[],bot=[];for(var i=0;i<=N;i++){var t=TMAX*i/N;top.push([px(t),py(hi(t))]);bot.push([px(t),py(lo(t))]);} var p='M'+top.map(function(q){return q[0].toFixed(1)+','+q[1].toFixed(1);}).join(' L')+' L'+bot.reverse().map(function(q){return q[0].toFixed(1)+','+q[1].toFixed(1);}).join(' L')+' Z';return'';} function draw(){var r=cifRel(mark),d=cifDeath(mark),s=surv(mark);var mx=px(mark).toFixed(1);plot.innerHTML=''+ band(function(){return 0;},cifRel,P.c0,0.5)+ band(cifRel,function(t){return cifRel(t)+cifDeath(t);},P.bad,0.45)+ band(function(t){return cifRel(t)+cifDeath(t);},function(){return 1;},P.acc,0.28)+''+'relapse'+'death'+'event-free'+'time (months)';read.innerHTML='At t = '+mark+' months: '+(s*100).toFixed(0)+'% event-free, '+(r*100).toFixed(0)+'% have relapsed, '+(d*100).toFixed(0)+'% have died. The three always sum to 100%: each event removes patients the other can no longer claim, which is why you share the risk with a CIF instead of treating each cause on its own.';} u.wireSeg(el.querySelector('.cr-seg'),function(v){mark=parseFloat(v);draw();});draw();} function rcode(){return['# Competing risks: cumulative incidence when relapse and death compete.','library(survival)','set.seed(3)','n <- 250','event_t <- rexp(n, 0.08) # time to the first event','etype <- sample(1:2, n, TRUE, c(0.6, 0.4)) # 1 = relapse, 2 = death','cens_t <- runif(n, 0, 30)','time <- pmin(event_t, cens_t)','status <- ifelse(event_t <= cens_t, etype, 0) # 0 = censored','ev <- factor(status, 0:2, c("censored", "relapse", "death"))','','fit <- survfit(Surv(time, ev) ~ 1) # multi-state = competing risks','summary(fit, times = c(5, 10, 20))$pstate # CIF per state over time','# relapse and death incidences plus the event-free fraction sum to 1 at every time.'].join('\n');} window.LessonWidgets.register('competing-risks',mount);})();;(function(){'use strict';var u=window.LessonWidgets.u,P=u.P;function rng(seed){var s=seed>>>0;return function(){s=(s*1664525+1013904223)>>>0;return s/4294967296;};} function gauss(r){return Math.sqrt(-2*Math.log(Math.max(r(),1e-9)))*Math.cos(6.2831853*r());} function build(){var r=rng(4),cal=[],te=[],i,x;for(i=0;i<120;i++){x=10*r();cal.push([x,2+1.5*x+2*gauss(r)]);} for(i=0;i<120;i++){x=10*r();te.push([x,2+1.5*x+2*gauss(r)]);} return{cal:cal,te:te};} function ols(pts){var n=pts.length,mx=0,my=0,i;for(i=0;i'+'
'+'
'+ u.runnable(rcode(),{label:'Split conformal prediction: guaranteed-coverage band, base R'});var plot=el.querySelector('.cb-plot'),read=el.querySelector('.cb-read');var W=340,H=220,PAD=26;var ally=d.cal.concat(d.te).map(function(p){return p[1];});var ymin=Math.min.apply(null,ally)-1,ymax=Math.max.apply(null,ally)+1;function sx(x){return PAD+x/10*(W-PAD-8);} function sy(y){return H-PAD-(y-ymin)/(ymax-ymin)*(H-PAD-8);} function draw(){var a=LV[level].a,qhat=qConformal(resCal,a);var xs=[0,10];var band='';var line='';var inside=0;var dots=d.te.map(function(p){var yhat=f(p[0]),inb=Math.abs(p[1]-yhat)<=qhat;if(inb)inside++;return'';}).join('');var cover=inside/d.te.length;plot.innerHTML=''+''+''+ band+line+dots+'test points; red = outside band';read.innerHTML='At a target of '+LV[level].pct+' coverage, the calibration residuals set the band half-width to q̂ = '+qhat.toFixed(2)+'. On fresh test points the band actually covers '+Math.round(cover*100)+'%, at or above target: that is conformal prediction\'s finite-sample guarantee, with no assumption about the error shape. Raise the target and the band widens to keep the promise.';} u.wireSeg(el.querySelector('.cb-seg'),function(v){level=v;draw();});draw();} function rcode(){return['# Split conformal prediction: a band with GUARANTEED coverage, from residual quantiles. Base R.','set.seed(1)','n <- 600; x <- runif(n, 0, 10); y <- 2 + 1.5 * x + rnorm(n, 0, 2)','d <- data.frame(x, y)','tr <- 1:300; cal <- 301:450; te <- 451:600 # train / calibrate / test','fit <- lm(y ~ x, d[tr, ]) # 1) fit on the training split','res <- abs(d$y[cal] - predict(fit, d[cal, ])) # 2) nonconformity = |residual| on calibration','alpha <- 0.1 # want >= 90% coverage','k <- ceiling((length(cal) + 1) * (1 - alpha)) / length(cal)','qhat <- quantile(res, k) # 3) the conformal quantile = band half-width','pred <- predict(fit, d[te, ]) # 4) band is pred +/- qhat on new points','cover <- mean(d$y[te] >= pred - qhat & d$y[te] <= pred + qhat)','round(c(qhat = unname(qhat), target = 1 - alpha, coverage = cover), 3)','#> qhat target coverage','#> 3.615 0.900 0.913'].join('\n');} window.LessonWidgets.register('conformal-bands',mount);})();;(function(){'use strict';function mount(el,cfg){var u=window.LessonWidgets.u;if(!u)return;cfg=cfg||{};var n=cfg.n||5,step=0;var CODE='for (i in 1:'+n+') {\n if (i %% 2 == 0) {\n print("even")\n } else {\n print("odd")\n }\n}';var wrap=document.createElement('div');wrap.style.cssText='font-family:IBM Plex Sans,system-ui,sans-serif';function codePanel(){var even=step>0&&step%2===0;var hiEven=even?u.P.add:'transparent',hiOdd=(step>0&&!even)?u.P.add:'transparent';return'
'+'for (i in 1:'+n+') {\n'+'  if (i %% 2 == 0) {\n'+'    print("even")\n'+'  } else {\n'+'    print("odd")\n'+'  }\n}
';} function statePanel(){var out=[];for(var k=1;k<=step;k++)out.push('[1] "'+(k%2===0?'even':'odd')+'"');var iNow=step===0?'–':step;return'
'+'i = '+iNow+(step===0?'':(step%2===0?' (even)':' (odd)'))+''+''+(step>=n?'loop finished':(step===0?'press Step to start':'press Step for i = '+(step+1)))+''+'
'+'
Console
'+'
'+(out.join('\n')||'(empty)')+'
';} function render(){wrap.querySelector('.cf-code').innerHTML=codePanel();wrap.querySelector('.cf-state').innerHTML=statePanel();wrap.querySelector('.cf-step').disabled=step>=n;wrap.querySelector('.cf-step').style.opacity=step>=n?'.5':'1';} wrap.innerHTML='
'+'
'+'
'+'
'+''+''+'
'+'
'+u.runnable(CODE,{label:'Run the whole loop'})+'
';wrap.querySelector('.cf-step').addEventListener('click',function(){if(step=0)return'rgba(31,122,85,'+(0.12+0.78*r).toFixed(2)+')';return'rgba(37,99,168,'+(0.12+0.78*(-r)).toFixed(2)+')';} var h='
';vars.forEach(function(v){h+='';});h+='';M.forEach(function(row,i){h+='';row.forEach(function(r){var dark=Math.abs(r)>0.55;h+='';});h+='';});h+='
'+u.esc(v)+'
'+u.esc(vars[i])+''+(Math.round(r*100)/100)+'
'+'
'+'−1+1'+'blue = negative, green = positive, intensity = strength
';el.innerHTML='
'+h+'
';} if(window.LessonWidgets)window.LessonWidgets.register('correlation-heatmap',mount);})();;(function(){'use strict';var u=window.LessonWidgets.u,P=u.P;var OBS=[0.34,0.16,0.15,0.12,0.09,0.06,0.035,0.02,0.01,0.005];var LAM=2.3;function pois(k){var e=Math.exp(-LAM),p=e;for(var i=1;i<=k;i++)p*=LAM/i;return p;} function nb(k){var r=2.0,pr=r/(r+LAM),c=1;for(var i=0;i'+'
'+'
'+ u.runnable(rcode(),{label:'Observed zeros vs what a Poisson predicts (base R)'});var plot=el.querySelector('.cd-plot'),read=el.querySelector('.cd-read');var W=440,H=220,m={l:36,r:12,t:12,b:28},iw=W-m.l-m.r,ih=H-m.t-m.b,mx=0.4;var bw=iw/OBS.length;function sy(p){return m.t+ih-p/mx*ih;} function draw(){var fn=model==='pois'?pois:model==='nb'?nb:zip;var svg='';svg+='';OBS.forEach(function(o,k){var x=m.l+bw*k+3,h=o/mx*ih;svg+='';svg+=''+k+'';});var pts='';for(var k=0;k';for(var k2=0;k2';svg+='bars = observed, line = model';svg+='';plot.innerHTML=svg;var gapZero=(OBS[0]-fn(0)).toFixed(2);read.innerHTML=model==='pois'?'Poisson misses on both ends. It underpredicts the zero bar by '+gapZero+' and cannot match the long tail, because Poisson forces variance = mean. Real counts are usually more spread out.':model==='nb'?'Negative binomial adds a dispersion parameter, so variance can exceed the mean. The tail now fits, but a genuine excess of zeros can still be under-modeled.':'Zero-inflated mixes a "structural zero" process with the counts, so the tall zero bar and the tail both fit: the right choice when many zeros come from "never at risk" cases.';} u.wireSeg(el.querySelector('.cd-seg'),function(v){model=v;draw();});draw();} function rcode(){return['# Does a Poisson fit? Compare the zeros it predicts to the zeros you actually see.','set.seed(1); n <- 800','# a process with extra zeros: 25% structural zeros, rest ~ Poisson(2.3)','lambda <- 2.3','y <- ifelse(runif(n) < 0.25, 0, rpois(n, lambda))','','obs_zeros <- mean(y == 0)','pois_zeros <- dpois(0, mean(y)) # zeros a fitted Poisson expects','c(observed = round(obs_zeros, 3), poisson_expects = round(pois_zeros, 3))','# far more observed zeros than Poisson allows -> reach for NB or a zero-inflated model.'].join('\n');} window.LessonWidgets.register('count-dist',mount);})();;(function(){'use strict';var u=window.LessonWidgets.u,P=u.P;var RHOS={weak:0.3,moderate:0.6,strong:0.85};function mount(el,cfg){cfg=cfg||{};var lvl='moderate';el.style.cssText='border:1px solid '+P.line+';border-radius:12px;background:#fff;padding:16px 17px';el.innerHTML='
'+u.seg([{v:'weak',label:'Weak (rho=0.3)'},{v:'moderate',label:'Moderate (0.6)'},{v:'strong',label:'Strong (0.85)'}],'moderate')+'
'+'
'+'
'+ u.runnable(rcode(),{label:'CUPED variance reduction, base R'});var plot=el.querySelector('.cu-plot'),read=el.querySelector('.cu-read');var W=340,H=130,CX=W/2,RAW_HALF=120;function bar(y,half,col,lab){return''+''+''+''+''+lab+'';} function draw(){var rho=RHOS[lvl],factor=Math.sqrt(1-rho*rho),cupHalf=RAW_HALF*factor;var reduct=Math.round(rho*rho*100);plot.innerHTML=''+''+ bar(46,RAW_HALF,P.mut,'raw 95% CI')+ bar(96,cupHalf,P.c0,'CUPED 95% CI')+'';read.innerHTML='A pre-period covariate correlated '+rho+' with the outcome shrinks the estimator variance by 1 − '+rho+'² = '+(1-rho*rho).toFixed(2)+', so the CI narrows by a factor of '+factor.toFixed(2)+' (a '+reduct+'% variance cut) with no bias. '+ (lvl==='weak'?'A weak pre-period signal barely helps.':lvl==='strong'?'A strong pre-period signal is like running the experiment on a much larger sample, for free.':'Equivalent to collecting '+Math.round(1/(1-rho*rho))+'x the sample at no extra cost.');} u.wireSeg(el.querySelector('.cu-seg'),function(v){lvl=v;draw();});draw();} function rcode(){return['# CUPED: use a pre-experiment covariate to shrink the variance of an A/B estimate. Base R.','set.seed(1)','n <- 4000','pre <- rnorm(n) # a metric measured BEFORE the test','rho <- 0.6 # its correlation with the outcome','y <- rho * pre + sqrt(1 - rho^2) * rnorm(n) # outcome, partly predicted by pre','arm <- rep(0:1, each = n / 2)','y <- y + 0.05 * arm # a true treatment lift of 0.05','','g <- n / 2','raw_se <- sqrt(var(y[arm == 1]) / g + var(y[arm == 0]) / g) # ordinary difference in means','','theta <- cov(y, pre) / var(pre) # CUPED: regress out the pre-covariate','yc <- y - theta * (pre - mean(pre))','cuped_se <- sqrt(var(yc[arm == 1]) / g + var(yc[arm == 0]) / g)','','round(c(raw_se = raw_se, cuped_se = cuped_se,',' ratio = cuped_se / raw_se, predicted = sqrt(1 - rho^2)), 3)','# the SE ratio lands right on sqrt(1 - rho^2): a real variance cut, no bias.'].join('\n');} window.LessonWidgets.register('cuped-variance',mount);})();;(function(){'use strict';var u=window.LessonWidgets.u,P=u.P;var SC=[0.94,1.08,0.86,1.15,0.97,1.03,0.9,1.11,0.99,0.92];function mount(el,cfg){cfg=cfg||{};var k=[4,5,10].indexOf(+cfg.k)>=0?+cfg.k:5,cur=1,N=20;el.style.cssText='border:1px solid '+P.line+';border-radius:12px;background:#fff;padding:16px 17px';el.innerHTML='
'+u.seg([{v:'4',label:'4 folds'},{v:'5',label:'5 folds'},{v:'10',label:'10 folds'}],String(k))+'
'+'
'+'
'+''+''+''+'
'+'
'+'
'+ u.runnable(rcode(),{label:'Run k-fold cross-validation in R'});var strip=el.querySelector('.cv-strip'),scores=el.querySelector('.cv-scores'),read=el.querySelector('.cv-read'),pos=el.querySelector('.cv-pos');function foldOf(i){return Math.floor(i*k/N)+1;} function draw(){var cells='';for(var i=0;i';} strip.innerHTML='
'+cells+'
'+'
■ validation fold■ training folds
';var used=SC.slice(0,k),mean=used.reduce(function(a,b){return a+b;},0)/k;scores.innerHTML=used.map(function(s,i){var on=(i+1)===cur;return'f'+(i+1)+' '+s.toFixed(2)+'';}).join('')+'CV mean '+mean.toFixed(3)+'';pos.textContent='fold '+cur+' of '+k;read.innerHTML='Fold '+cur+' is held out for validation; the other '+(k-1)+' folds train the model, giving score '+used[cur-1].toFixed(2)+'. Every point validates exactly once. The average of all '+k+' is the cross-validated error - steadier and less luck-dependent than any single split. More folds = less bias, more compute.';} u.wireSeg(el.querySelector('.cv-seg'),function(v){k=+v;cur=1;draw();});el.querySelector('.cv-next').addEventListener('click',function(){cur=cur%k+1;draw();});el.querySelector('.cv-prev').addEventListener('click',function(){cur=(cur-2+k)%k+1;draw();});draw();} function rcode(){return['# k-fold cross-validation by hand: each fold takes a turn as the holdout.','set.seed(1)','n <- 100','df <- data.frame(x = rnorm(n)); df$y <- 2 * df$x + rnorm(n)','','k <- 5','fold <- sample(rep(1:k, length.out = n)) # random fold labels','rmse <- numeric(k)','for (f in 1:k) {',' tr <- df[fold != f, ]; va <- df[fold == f, ]',' fit <- lm(y ~ x, data = tr)',' rmse[f] <- sqrt(mean((va$y - predict(fit, va))^2))','}','rmse # one score per fold','mean(rmse) # the cross-validated error'].join('\n');} window.LessonWidgets.register('cv-folds',mount);})();;(function(){'use strict';var u=window.LessonWidgets.u;var LIGHT={ink:'#131720',body:'#434b59',mut:'#677084',faint:'#97a0b2',line:'#d8dee9',grid:'#eef1f6',acc:'#1f7a55',c0:'#2563a8',c1:'#b5631a',bad:'#c2410c',panel:'#ffffff',soft:'#f3f6f4',nodeFill:'#eef1f7'};var DARK={ink:'#eef4fb',body:'#c3d1e3',mut:'#93a4bb',faint:'#6f8299',line:'rgba(255,255,255,.24)',grid:'rgba(255,255,255,.10)',acc:'#46c08a',c0:'#7fb2ea',c1:'#e3a05a',bad:'#f4805a',panel:'#101c2b',soft:'rgba(255,255,255,.05)',nodeFill:'#1d2c40'};function lumOf(c){c=String(c).trim();var r,g,b,m;if(c.charAt(0)==='#'){if(c.length===4){r=parseInt(c[1]+c[1],16);g=parseInt(c[2]+c[2],16);b=parseInt(c[3]+c[3],16);} else{r=parseInt(c.substr(1,2),16);g=parseInt(c.substr(3,2),16);b=parseInt(c.substr(5,2),16);}}else if((m=c.match(/rgba?\(([^)]+)\)/))){var p=m[1].split(',');r=+p[0];g=+p[1];b=+p[2];} else return 1;return(0.2126*r+0.7152*g+0.0722*b)/255;} function isDark(el){try{var v=getComputedStyle(el).getPropertyValue('--lm-panel');if(v&&v.trim())return lumOf(v)<0.45;}catch(e){} return!!(document.documentElement&&document.documentElement.classList.contains('dark'));} function palette(el){return isDark(el)?DARK:LIGHT;} var SCEN={confounding:{label:'Confounding',ask:'A reviewer says the groups differed on something that also drives the outcome.',order:['Z','X','M','Y'],nodes:{Z:{pos:[110,38],name:'baseline severity'},X:{pos:[58,152],name:'treatment'},M:{pos:[260,152],name:'adherence'},Y:{pos:[430,100],name:'outcome'}},X:'X',Y:'Y',edges:[{f:'Z',t:'X',b:0.60,on:true},{f:'Z',t:'Y',b:0.65,on:true},{f:'X',t:'Y',b:0.30,on:true},{f:'X',t:'M',b:0.55,on:false},{f:'M',t:'Y',b:0.40,on:false}],adjust:[]},baseline:{label:'Baseline difference',ask:'A reviewer says the randomised arms were not balanced at baseline.',order:['A','B','X','Y'],nodes:{A:{pos:[70,38],name:'age'},B:{pos:[252,38],name:'baseline score'},X:{pos:[70,152],name:'randomised arm'},Y:{pos:[430,110],name:'follow-up score'}},X:'X',Y:'Y',edges:[{f:'B',t:'X',b:0.22,on:true},{f:'B',t:'Y',b:0.70,on:true},{f:'X',t:'Y',b:0.30,on:true},{f:'A',t:'B',b:0.50,on:false},{f:'A',t:'Y',b:0.30,on:false}],adjust:[]},selection:{label:'Selection bias',ask:'A reviewer says the analysed sample is not the sample you recruited.',order:['U','X','Y','S'],nodes:{U:{pos:[252,30],name:'motivation',latent:true},X:{pos:[64,128],name:'treatment'},Y:{pos:[440,128],name:'outcome'},S:{pos:[252,186],name:'stayed in the study'}},X:'X',Y:'Y',edges:[{f:'X',t:'Y',b:0.30,on:true},{f:'X',t:'S',b:0.50,on:true},{f:'Y',t:'S',b:0.50,on:true},{f:'U',t:'X',b:0.40,on:false},{f:'U',t:'Y',b:0.40,on:false}],adjust:['S']},'non-comparable':{label:'Non-comparable controls',ask:'A reviewer says the controls came from somewhere the cases did not.',order:['U','P','X','Y'],nodes:{U:{pos:[252,30],name:'true case mix',latent:true},P:{pos:[252,128],name:'recorded case mix'},X:{pos:[64,128],name:'came from clinic A'},Y:{pos:[440,128],name:'outcome'}},X:'X',Y:'Y',edges:[{f:'U',t:'P',b:0.80,on:true},{f:'U',t:'X',b:0.60,on:true},{f:'U',t:'Y',b:0.65,on:true},{f:'X',t:'Y',b:0.30,on:true},{f:'P',t:'X',b:0.35,on:false}],adjust:[]}};function covariance(sc,on){var order=sc.order,k=order.length,ix={},i,j,a,b2;for(i=0;iMath.abs(M[piv][i]))piv=k2;var t=M[i];M[i]=M[piv];M[piv]=t;var d=M[i][i]||1e-300;for(j=i;j<=p;j++)M[i][j]/=d;for(k2=0;k2'+e.t]=e.b;out.adjOut[e.f].push(e.t);out.adjIn[e.t].push(e.f);out.nbr[e.f].push(e.t);out.nbr[e.t].push(e.f);});out.desc={};Object.keys(sc.nodes).forEach(function(v){var seen={},stack=out.adjOut[v].slice();while(stack.length){var w=stack.pop();if(seen[w])continue;seen[w]=1;out.adjOut[w].forEach(function(z){stack.push(z);});} out.desc[v]=Object.keys(seen);});return out;} function allPaths(G,from,to){var res=[];(function walk(v,path,used){if(v===to){res.push(path.slice());return;} G.nbr[v].forEach(function(w){if(used[w])return;used[w]=1;path.push(w);walk(w,path,used);path.pop();used[w]=0;});})(from,[from],(function(){var s={};s[from]=1;return s;})());return res;} function classify(sc,G,path,adjSet){var i,blockedBy=null,opener=null,causal=true,forward=[];for(i=0;i'+path[i+1])in G.has;forward.push(fwd);if(!fwd)causal=false;} for(i=1;i'+v)in G.has,inB=(next+'>'+v)in G.has;if(inA&&inB){var openIt=adjSet[v];if(!openIt)for(var d=0;d'+path[0])in G.has;return{path:path,forward:forward,causal:causal,backdoor:backdoor,open:!blockedBy,blockedBy:blockedBy,opener:opener};} function totalEffect(sc,G){var t=0;allPaths(G,sc.X,sc.Y).forEach(function(p){var prod=1,ok=true,i;for(i=0;i'+p[i+1]];if(b===undefined){ok=false;break;} prod*=b;} if(ok)t+=prod;});return t;} function mount(el,cfg){cfg=cfg||{};var list=(cfg.scenarios&&cfg.scenarios.length?cfg.scenarios:(cfg.scenario?[cfg.scenario]:Object.keys(SCEN))).filter(function(s){return SCEN[s];});if(!list.length)list=['confounding'];var nSim=Math.max(2000,+cfg.n||100000);var key=list[0],sc,on,adjSet;function reset(){sc=SCEN[key];on=sc.edges.map(function(e){return!!e.on;});adjSet={};sc.adjust.forEach(function(v){adjSet[v]=true;});} reset();var P=palette(el);el.style.cssText='border:1px solid '+P.line+';border-radius:12px;background:'+P.panel+';padding:16px 17px';el.innerHTML=(list.length>1?'
':'')+'
'+'
'+'Click a node to adjust for it. Click an arrow to remove it, or a ghosted arrow to add it.
'+'
'+'
'+'
'+'
'+''+'
';var segBox=el.querySelector('.dg-seg'),plot=el.querySelector('.dg-plot'),numBox=el.querySelector('.dg-num'),pathBox=el.querySelector('.dg-paths'),msg=el.querySelector('.dg-msg'),ask=el.querySelector('.dg-ask'),rbox=el.querySelector('.dg-r'),resetBtn=el.querySelector('.dg-reset');if(segBox){segBox.innerHTML=u.seg(list.map(function(s){return{v:s,label:SCEN[s].label};}),key);u.wireSeg(segBox,function(v){key=v;reset();render('');});} resetBtn.addEventListener('click',function(){reset();render('Back to the diagram as the reviewer described it.');});plot.addEventListener('click',function(ev){var g=ev.target.closest?ev.target.closest('[data-hit]'):null;if(!g||!plot.contains(g))return;act(g.getAttribute('data-hit'));});plot.addEventListener('keydown',function(ev){if(ev.key!=='Enter'&&ev.key!==' ')return;var g=ev.target.closest?ev.target.closest('[data-hit]'):null;if(!g||!plot.contains(g))return;ev.preventDefault();act(g.getAttribute('data-hit'));});function act(hit){var bits=hit.split(':');if(bits[0]==='n'){var v=bits[1];if(v===sc.X||v===sc.Y){render('The exposure and the outcome are what you are relating. You do not adjust for either.');return;} if(sc.nodes[v].latent){render(sc.nodes[v].name+' is unmeasured. That is the whole difficulty: you cannot adjust for a column you do not have.');return;} adjSet[v]=!adjSet[v];render(adjSet[v]?'Adjusting for '+sc.nodes[v].name+'.':'No longer adjusting for '+sc.nodes[v].name+'.');}else{var q=+bits[1];on[q]=!on[q];var e=sc.edges[q];render((on[q]?'Added ':'Removed ')+e.f+' to '+e.t+'.');}} function render(note){P=palette(el);el.style.background=P.panel;el.style.borderColor=P.line;ask.style.color=P.body;msg.style.color=P.body;var cov=covariance(sc,on),G=graph(sc,on);var adj=Object.keys(adjSet).filter(function(v){return adjSet[v]&&!sc.nodes[v].latent;});var est=estimate(sc,cov,adj),truth=totalEffect(sc,G);var crude=estimate(sc,cov,[]);var paths=allPaths(G,sc.X,sc.Y).map(function(p){return classify(sc,G,p,adjSet);});ask.textContent=sc.ask;plot.innerHTML=drawDag(sc,on,adjSet,P);numBox.innerHTML=numbers(est,truth,adj,sc,P);pathBox.innerHTML=pathList(paths,P);msg.innerHTML=(note?''+u.esc(note)+' ':'')+explain(paths,est,truth,crude,adj,sc,P);rbox.innerHTML=u.runnable(rcode(sc,on,adj,cov,truth,nSim),{label:'Simulate this exact diagram in R'});} var wasDark=isDark(el);if(window.MutationObserver){var mo=new MutationObserver(function(){if(!document.contains(el)){mo.disconnect();return;} var now=isDark(el);if(now!==wasDark){wasDark=now;render('');}});mo.observe(document.documentElement,{attributes:true,attributeFilter:['class']});} render('');} var R=25;function drawDag(sc,on,adjSet,P){var W=520,H=232,g='';function pos(v){return sc.nodes[v].pos;} sc.edges.forEach(function(e,q){var a=pos(e.f),b=pos(e.t),dx=b[0]-a[0],dy=b[1]-a[1];var L=Math.sqrt(dx*dx+dy*dy)||1,ux=dx/L,uy=dy/L;var x1=a[0]+ux*(R+2),y1=a[1]+uy*(R+2),x2=b[0]-ux*(R+7),y2=b[1]-uy*(R+7);var live=on[q],col=live?P.mut:P.faint,ang=Math.atan2(uy,ux);g+=''+''+''+'';if(live&&e.b){var mx=(x1+x2)/2,my=(y1+y2)/2;g+=''+e.b.toFixed(2)+'';} g+='';});Object.keys(sc.nodes).forEach(function(v){var p=sc.nodes[v].pos,node=sc.nodes[v];var isX=v===sc.X,isY=v===sc.Y,adj=!!adjSet[v]&&!node.latent;var fill=isX?P.acc:(isY?P.c0:P.nodeFill);var txt=(isX||isY)?'#fff':P.ink;g+='';if(adj)g+='';g+=''+''+u.esc(v)+''+''+u.esc(node.name)+(node.latent?' (unmeasured)':'')+''+'';});return''+g+'';} function numbers(est,truth,adj,sc,P){var bias=est-truth,off=Math.abs(bias)>0.02;var W=520,x0=162,span=288;var lo=Math.min(-0.15,est-0.12,truth-0.12),hi=Math.max(0.95,est+0.15,truth+0.15);function bx(v){return x0+(Math.max(lo,Math.min(hi,v))-lo)/(hi-lo)*span;} var g='';function valLabel(v,y,col){var neg=v<0;return''+v.toFixed(3)+'';} g+='True effect of X on Y'+''+ valLabel(truth,10,P.acc);g+='What the model reports'+''+ valLabel(est,36,off?P.bad:P.acc);g+=''+ u.esc('lm('+sc.Y+' ~ '+[sc.X].concat(adj).join(' + ')+')')+''+''+'bias '+(bias>=0?'+':'')+bias.toFixed(3)+'';return''+g+'';} function pathList(paths,P){var rows=paths.slice().sort(function(a,b){return(b.causal?1:0)-(a.causal?1:0);}).map(function(p){var s=pathString(p);var open=p.open,kind=p.causal?'causal':(p.backdoor?'backdoor':'non-causal');var colour=p.causal?(open?P.acc:P.c1):(open?P.bad:P.mut);var state=p.causal?(open?'carrying the effect':'blocked, so this part of the effect is removed from the estimate'):(open?'OPEN, so it leaks into the estimate':'blocked');if(!p.causal&&open&&p.opener)state='OPEN because you adjusted for '+p.opener+', a collider';if(!p.causal&&!open&&p.blockedBy&&p.blockedBy.kind==='collider')state='blocked by the collider at '+p.blockedBy.node;if(!p.causal&&!open&&p.blockedBy&&p.blockedBy.kind==='adjusted')state='blocked by adjusting for '+p.blockedBy.node;return'
  • '+s+' '+kind+', '+state+'
  • ';});if(!rows.length)return'
    No path connects X and Y at all.
    ';return'
    Every route between X and Y
    '+'
      '+rows.join('')+'
    ';} function pathString(p){var s=p.path[0];for(var i=0;i'+est.toFixed(3)+' against a true '+truth.toFixed(3)+'.';} var bits=[];if(openBack.length){bits.push(openBack.length===1?'One non-causal route is still open ('+pathString(openBack[0])+'), and it is carrying '+ (bias>=0?'+':'')+bias.toFixed(3)+' into the coefficient':openBack.length+' non-causal routes are open, and together they move the coefficient by '+(bias>=0?'+':'')+bias.toFixed(3));} if(blockedCausal.length){bits.push('a causal route is blocked by the adjustment, so the number on screen is a direct effect, not the total effect the question asked about');} var tail='';var latent=Object.keys(sc.nodes).filter(function(v){return sc.nodes[v].latent;});if(openBack.length&&latent.length&&openBack.some(function(p){return p.path.indexOf(latent[0])>=0;})){tail=' The open route runs through '+latent[0]+', which you did not measure, so nothing in this dataset closes it.';if(adj.length&&Math.abs(est-truth)'+u.esc(cfg.filterLabel||'Region')+':reactive'+'
    '+'
    ';var segHost=wrap.querySelector('.db-seg');segHost.innerHTML=u.seg(names,cur);var boxesEl=wrap.querySelector('.db-boxes'),tilesEl=wrap.querySelector('.db-tiles');function tile(title,inner){return'
    '+u.esc(title)+'
    '+inner+'
    ';} function render(v){var view=views[v];boxesEl.innerHTML=view.boxes.map(function(b){return'
    '+u.esc(b[0])+'
    '+u.esc(b[1])+'
    ';}).join('');tilesEl.innerHTML=tile('Revenue trend',u.plot(view.line,{geom:'line',x:'month',y:'k$',w:320,h:180}))+tile('By channel',u.plot(view.bar,{geom:'bar',x:'',y:'k$',w:320,h:180}));} u.wireSeg(segHost,function(v){render(v);});render(cur);el.innerHTML='';el.appendChild(wrap);} if(window.LessonWidgets)window.LessonWidgets.register('dashboard-layout',mount);})();;(function(){'use strict';var u=window.LessonWidgets.u,P=u.P;function mount(el,cfg){cfg=cfg||{};var N=20,nTrain=12,nVal=4;var leak=false;el.style.cssText='border:1px solid '+P.line+';border-radius:12px;background:#fff;padding:16px 17px';el.innerHTML='
    '+'
    '+''+'
    '+'
    '+ u.runnable(rcode(),{label:'Split honestly - then watch a leak inflate the score - in R'});var strip=el.querySelector('.ds-strip'),read=el.querySelector('.ds-read');function draw(){var cells='';for(var i=0;i';} var leakCol=leak?'
    + leaked column (a near-copy of y)
    '+'
    '+Array.apply(null,Array(N)).map(function(){return'
    ';}).join('')+'
    ':'';strip.innerHTML='
    '+cells+'
    '+'
    '+'■ train 60%■ validation 20%■ test 20%
    '+leakCol;read.innerHTML=leak?'Test accuracy 0.99 - too good to be true. The leaked column is basically the answer, so the model "predicts" by copying it. On truly new data with no such column, it collapses. Leakage = any information at training time you would not have at prediction time. Split FIRST, engineer features INSIDE the training fold only.':'Honest test accuracy 0.78. Fit on train, tune on validation, and touch test exactly once at the end. The test set stands in for the future: the moment it influences a choice, its score stops being trustworthy.';} el.querySelector('.ds-leak').addEventListener('change',function(e){leak=e.target.checked;draw();});draw();} function rcode(){return['# An honest split, then a demonstration of leakage.','set.seed(1)','n <- 200','df <- data.frame(x = rnorm(n), y = rbinom(n, 1, 0.5))','','idx <- sample(n, 0.7 * n) # 70% to train, the rest to test','train <- df[idx, ]; test <- df[-idx, ]','c(train = nrow(train), test = nrow(test))','','# LEAKAGE: a feature secretly built from the answer y','df$leak <- df$y + rnorm(n, 0, 0.01) # "leak" basically IS y','fit <- glm(y ~ leak, family = binomial, data = df[idx, ])','pred <- predict(fit, df[-idx, ], type = "response") > 0.5','mean(pred == df$y[-idx]) # ~1.00 - the tell-tale sign of a leak'].join('\n');} window.LessonWidgets.register('data-split',mount);})();;(function(){'use strict';function mount(el,cfg){var u=window.LessonWidgets.u;if(!u)return;cfg=cfg||{};var COLS=cfg.columns||[{name:'name',type:'character',values:['Ana','Ben','Cy','Devi']},{name:'age',type:'integer',values:[29,41,35,23]},{name:'member',type:'logical',values:['TRUE','FALSE','TRUE','TRUE']}];var on=COLS.map(function(){return true;});function active(){return COLS.filter(function(_,i){return on[i];});} function rLiteral(){var a=active();var lines=a.map(function(c){var vals=c.values.map(function(v){return c.type==='character'?'"'+v+'"':v;}).join(', ');return' '+c.name+' = c('+vals+')';});return'df <- data.frame(\n'+lines.join(',\n')+'\n)';} var wrap=document.createElement('div');wrap.style.cssText='font-family:IBM Plex Sans,system-ui,sans-serif';function render(){var a=active();var toggles=COLS.map(function(c,i){return'';}).join('');var tableHtml,dimHtml,typesHtml;if(!a.length){tableHtml='
    Turn on at least one column to build a data frame.
    ';dimHtml='';typesHtml='';}else{var cols=a.map(function(c){return c.name;});var rows=a[0].values.map(function(_,ri){return a.map(function(c){return c.values[ri];});});tableHtml=u.tbl(cols,rows);dimHtml='dim(df) #> '+rows.length+' × '+cols.length+'';typesHtml=a.map(function(c){return''+u.esc(c.name)+': '+c.type+'';}).join(' ');} wrap.querySelector('.dfb-toggles').innerHTML=toggles;wrap.querySelector('.dfb-table').innerHTML=tableHtml;wrap.querySelector('.dfb-meta').innerHTML=(a.length?'
    '+dimHtml+'
    '+typesHtml:'');wrap.querySelector('.dfb-run').innerHTML=a.length?u.runnable(rLiteral()+'\n\ndf\nstr(df)',{label:'Build it in R'}):'';} wrap.innerHTML='
    '+'
    Columns (each is one type; all the same length)
    '+'
    '+'
    '+'
    '+'
    '+'
    ';wrap.addEventListener('click',function(e){var b=e.target.closest('.dfb-col');if(!b)return;var i=+b.getAttribute('data-i');on[i]=!on[i];render();});el.innerHTML='';el.appendChild(wrap);render();} if(window.LessonWidgets)window.LessonWidgets.register('dataframe-builder',mount);})();;(function(){'use strict';var C0='#2563a8',C1='#b5631a';function mulberry32(a){return function(){a|=0;a=a+0x6D2B79F5|0;var t=Math.imul(a^a>>>15,1|a);t=t+Math.imul(t^t>>>7,61|t)^t;return((t^t>>>14)>>>0)/4294967296;};} function mount(el,cfg){cfg=cfg||{};var SEED=cfg.seed||7;var MIN=cfg.min||1,MAX=cfg.max||10,START=cfg.start||3;var SHOW_TEST=cfg.showTest!==false;var labels=cfg.labels||{c0:'class 0',c1:'class 1'};var rnd=mulberry32(SEED);function gauss(mx,my,s){var u=rnd(),v=rnd();var g=Math.sqrt(-2*Math.log(u+1e-9))*Math.cos(6.2832*v);var g2=Math.sqrt(-2*Math.log(u+1e-9))*Math.sin(6.2832*v);return{x:Math.min(0.97,Math.max(0.03,mx+g*s)),y:Math.min(0.97,Math.max(0.03,my+g2*s))};} var DATA=[];(function(){function blob(mx,my,c,n){for(var k=0;k=pts.length-c0)?0:1;if(depth>=maxD||pts.length<4||c0===0||c0===pts.length)return{leaf:true,cls:maj};var best=null;['x','y'].forEach(function(ax){var vals=pts.map(function(p){return p[ax];}).sort(function(a,b){return a-b;});for(var v=0;v'+''+'
    '+'
    '+'
    '+labels.c0+''+''+labels.c1+''+ (SHOW_TEST?'held-out test point':'')+'
    '+'
    '+'
    .
    Train accuracy
    '+ (SHOW_TEST?'
    .
    Test accuracy
    ':'')+'
    '+'
    '+'
    ';var slider=el.querySelector('.lw-slider');var depthEl=el.querySelector('.lw-depth');var canvas=el.querySelector('.lw-canvas');var trainEl=el.querySelector('.lw-train');var testEl=el.querySelector('.lw-test');var noteEl=el.querySelector('.lw-note');var ctx=canvas.getContext('2d');function acc(tree,pts){var ok=0;pts.forEach(function(p){if(predict(tree,p.x,p.y)===p.c)ok++;});return pts.length?ok/pts.length:0;} function render(){var d=+slider.value;depthEl.textContent=d;var tree=build(TRAIN,0,d);var W=canvas.width,H=canvas.height,cell=6;for(var px=0;px0.13){verdict='Overfitting: it is memorizing noise. Train is high, test has dropped.';vclass='lw-over';} else{verdict='Good fit: the boundary is about as accurate on unseen data as on training data.';vclass='lw-good';} noteEl.className='lw-note '+vclass;noteEl.innerHTML='Depth '+d+'. '+verdict;} slider.addEventListener('input',render);render();} if(window.LessonWidgets)window.LessonWidgets.register('decision-region',mount);})();;(function(){'use strict';function mulberry32(a){return function(){a|=0;a=a+0x6D2B79F5|0;var t=Math.imul(a^a>>>15,1|a);t=t+Math.imul(t^t>>>7,61|t)^t;return((t^t>>>14)>>>0)/4294967296;};} function mount(el,cfg){var FEATS=[{n:'tenure',s:0.92,c:'#2563a8'},{n:'monthly_spend',s:0.64,c:'#1f7a55'},{n:'support_calls',s:0.55,c:'#b5631a'},{n:'contract',s:0.42,c:'#8a46b0'},{n:'age',s:0.22,c:'#5a4fcf'}];var mtry=5;el.innerHTML='
    '+'
    '+'
    '+'
    Tree diversity'+'.
    '+'
    ';var host=el.querySelector('.lw-dtrees'),fill=el.querySelector('.lw-divfill'),pv=el.querySelector('.lw-divpv'),note=el.querySelector('.lw-note');function render(){var roots=[],html='';for(var t=0;t<6;t++){var rng=mulberry32(50+t*131+(mtry===5?0:324));var idx=[0,1,2,3,4];for(var s=4;s>0;s--){var j=(rng()*(s+1))|0;var tmp=idx[s];idx[s]=idx[j];idx[j]=tmp;} var subset=idx.slice(0,mtry),bf=subset[0];subset.forEach(function(f){if(FEATS[f].s>FEATS[bf].s)bf=f;});roots.push(bf);html+='
    tree '+(t+1)+'
    first split on
    '+'
    '+FEATS[bf].n+'
    ';} host.innerHTML=html;var distinct={};roots.forEach(function(r){distinct[r]=1;});var div=Object.keys(distinct).length,pct=Math.round((div-1)/5*100);fill.style.width=pct+'%';pv.textContent=div+'/6';note.innerHTML=(mtry===5)?'Bagging: every tree may use all features, so the strongest one, tenure, wins every first split. The trees are near-identical, correlated, and averaging barely helps.':'Random forest: each tree sees only 2 random features, so tenure often is not even an option. The first splits scatter across features, the trees decorrelate, and averaging pays off.';} Array.prototype.forEach.call(el.querySelectorAll('.lw-toggle button'),function(b){b.addEventListener('click',function(){mtry=+b.getAttribute('data-m');Array.prototype.forEach.call(el.querySelectorAll('.lw-toggle button'),function(x){x.classList.toggle('on',x===b);});render();});});render();} if(window.LessonWidgets)window.LessonWidgets.register('decorrelation',mount);})();;(function(){'use strict';var u=window.LessonWidgets.u,P=u.P;var COL=[P.c0,P.acc,(P.c2||'#c9a24a'),'#7a8a55','#9a6a9a'];var LEAVES=['A','B','C','D','E','F','G','H'];var MERGES=[{l:{leaf:0},r:{leaf:1},h:0.8},{l:{leaf:2},r:{leaf:3},h:1.0},{l:{leaf:5},r:{leaf:6},h:0.9},{l:{leaf:4},r:{m:2},h:1.6},{l:{m:0},r:{m:1},h:2.4},{l:{m:3},r:{leaf:7},h:2.9},{l:{m:4},r:{m:5},h:4.2}];function mount(el,cfg){cfg=cfg||{};var cut=3.4,HMAX=4.6;el.style.cssText='border:1px solid '+P.line+';border-radius:12px;background:#fff;padding:16px 17px';el.innerHTML='
    '+''+'
    '+ u.runnable(rcode(),{label:'Run hclust and cut the tree in R'});var plot=el.querySelector('.dn-plot'),read=el.querySelector('.dn-read'),slider=el.querySelector('.dn-s'),hEl=el.querySelector('.dn-h');var W=420,H=220,m=24,leafX={},lw=(W-2*m)/(LEAVES.length-1);LEAVES.forEach(function(_,i){leafX[i]=m+i*lw;});function px(node){return node.leaf!=null?leafX[node.leaf]:(px(MERGES[node.m].l)+px(MERGES[node.m].r))/2;} function py(h){return(H-22)-h/HMAX*(H-40);} function clustersBelow(){var id=LEAVES.map(function(_,i){return i;}),next=LEAVES.length;MERGES.forEach(function(mg,mi){if(mg.h<=cut){var li=repId(mg.l),ri=repId(mg.r);var a=id[li],b=id[ri];for(var k=0;k';MERGES.forEach(function(mg){var xl=px(mg.l),xr=px(mg.r),y=py(mg.h),yl=py(mg.l.leaf!=null?0:MERGES[mg.l.m].h),yr=py(mg.r.leaf!=null?0:MERGES[mg.r.m].h);var col=mg.h<=cut?COL[(mg._cl||0)%COL.length]:P.mut;svg+='';});LEAVES.forEach(function(lab,i){svg+=''+lab+'';});svg+='';svg+='';plot.innerHTML=svg;read.innerHTML='Cutting at height '+cut.toFixed(1)+' gives '+cl.k+' cluster'+(cl.k===1?'':'s')+'. Lower the cut for more, finer clusters; raise it to merge them.';hEl.textContent=cut.toFixed(1);} slider.addEventListener('input',function(){cut=+slider.value;draw();});draw();} function rcode(){return['# Hierarchical clustering builds a tree of merges; cut it at a height to get k groups.','d <- dist(scale(iris[, 1:4])) # pairwise distances','hc <- hclust(d, method = "ward.D2")','plot(hc, labels = FALSE) # the dendrogram','','groups <- cutree(hc, k = 3) # cut into 3 clusters','table(groups, iris$Species) # do the cuts match the species?'].join('\n');} window.LessonWidgets.register('dendrogram',mount);})();;(function(){'use strict';var u=window.LessonWidgets.u,P=u.P;var cPre=20,cPost=26,tPre=18,tPost=30;var cf=tPre+(cPost-cPre);var did=tPost-cf;var naive=tPost-cPost;function mount(el,cfg){cfg=cfg||{};var mode='did';el.style.cssText='border:1px solid '+P.line+';border-radius:12px;background:#fff;padding:16px 17px';el.innerHTML='
    '+u.seg([{v:'naive',label:'Naive (post only)'},{v:'did',label:'Difference-in-differences'}],'did')+'
    '+'
    '+'
    '+ u.runnable(rcode(),{label:'Difference-in-differences with lm(y ~ treat*post), base R'});var plot=el.querySelector('.dd-plot'),read=el.querySelector('.dd-read');var W=340,H=200,L=34,R=90,T=16,B=26;var xPre=L,xPost=W-R,ymin=14,ymax=34;function py(v){return T+(ymax-v)/(ymax-ymin)*(H-T-B);} function line(x1,y1,x2,y2,col,dash,wid){return'';} function dot(x,v,col){return'';} function draw(){var gapX=mode==='did'?xPost+14:xPost+14;var top=mode==='did'?tPost:tPost,bot=mode==='did'?cf:cPost;var svg='';svg+=line(xPre,cPre,xPost,cPost,P.c1)+dot(xPre,cPre,P.c1)+dot(xPost,cPost,P.c1);svg+=line(xPre,tPre,xPost,tPost,P.c0)+dot(xPre,tPre,P.c0)+dot(xPost,tPost,P.c0);if(mode==='did')svg+=line(xPre,tPre,xPost,cf,P.c0,true,2)+dot(xPost,cf,P.c0);svg+='';svg+=''+(mode==='did'?did:naive)+'';svg+='before';svg+='after';svg+='treated';svg+='control';svg+='';plot.innerHTML=svg;read.innerHTML=mode==='naive'?'The naive comparison subtracts the two post values: '+tPost+' − '+cPost+' = '+naive+'. But the groups began at different levels ('+tPre+' vs '+cPre+'), so this is biased.':'The dashed line is the counterfactual: where treated would land if it moved parallel to control ('+tPre+' + '+(cPost-cPre)+' = '+cf+'). DiD = '+tPost+' − '+cf+' = '+did+', the double difference, valid only if the trends really are parallel.';} u.wireSeg(el.querySelector('.dd-seg'),function(v){mode=v;draw();});draw();} function rcode(){return['# Difference-in-differences: two groups, before and after a policy. Base R.','set.seed(1)','n <- 200','d <- expand.grid(unit = 1:n, post = 0:1)','d$treat <- ifelse(d$unit <= n / 2, 0, 1)','base <- ifelse(d$treat == 1, 18, 20) # treated start lower','trend <- ifelse(d$post == 1, 6, 0) # common time trend','effect <- ifelse(d$treat == 1 & d$post == 1, 6, 0) # TRUE DiD effect = 6','d$y <- base + trend + effect + rnorm(nrow(d), 0, 4)','','cell <- tapply(d$y, list(treat = d$treat, post = d$post), mean)','did <- (cell["1","1"] - cell["1","0"]) - (cell["0","1"] - cell["0","0"])','c(naive_post_gap = cell["1","1"] - cell["0","1"], did = did)','#> naive_post_gap did','#> 4.09 6.67','','round(coef(lm(y ~ treat * post, data = d)), 2) # the treat:post term IS the DiD','#> (Intercept) treat post treat:post','#> 20.44 -2.59 5.68 6.67'].join('\n');} window.LessonWidgets.register('did-parallel',mount);})();;(function(){'use strict';function mount(el,cfg){var u=window.LessonWidgets.u;if(!u)return;cfg=cfg||{};var blocks=cfg.blocks||[{type:'yaml',text:'title: "Q4 Sales report"\nauthor: "Analytics"\nformat: html'},{type:'prose',text:'## Summary\n\nRevenue grew **18%** in Q4, led by the Web channel.'},{type:'code',text:'ggplot(sales, aes(channel, rev)) +\n geom_col()',chart:[{x:'Web',y:62},{x:'Retail',y:40},{x:'B2B',y:38}]}];var P=u.P;function mdInline(t){return u.esc(t).replace(/\*\*(.+?)\*\*/g,'$1');} function source(){var h='';blocks.forEach(function(b){if(b.type==='yaml')h+='
    ---\n'+u.esc(b.text)+'\n---
    ';else if(b.type==='code')h+='
    ```{r}
    '+u.code(b.text)+'
    ```
    ';else h+='
    '+u.esc(b.text)+'
    ';});return'
    '+h+'
    ';} function rendered(){var h='
    ';blocks.forEach(function(b){if(b.type==='yaml'){var m=/title:\s*"?([^"\n]+)"?/.exec(b.text);h+='
    '+u.esc(m?m[1]:'Document')+'
    ';} else if(b.type==='prose'){b.text.split(/\n\n+/).forEach(function(para){var hm=/^##\s+(.+)/.exec(para);if(hm)h+='
    '+mdInline(hm[1])+'
    ';else h+='

    '+mdInline(para)+'

    ';});} else if(b.type==='code'&&b.chart){h+=u.plot(b.chart,{geom:'bar',x:'',y:'',w:400,h:180});}});return h+'
    ';} var wrap=document.createElement('div');wrap.style.cssText='font-family:IBM Plex Sans,system-ui,sans-serif';wrap.innerHTML='
    ';var segHost=wrap.querySelector('.ds-seg');segHost.innerHTML=u.seg([{v:'src',label:'Source (.qmd)'},{v:'out',label:'Rendered'}],'src');var stage=wrap.querySelector('.ds-stage');function render(v){stage.innerHTML=v==='out'?rendered():source();} u.wireSeg(segHost,function(v){render(v);});render('src');el.innerHTML='';el.appendChild(wrap);} if(window.LessonWidgets)window.LessonWidgets.register('doc-structure',mount);})();;(function(){'use strict';var u=window.LessonWidgets.u,P=u.P;var BINS=10,REF=[];(function(){var s=6;function r(){s=(s*1103515245+12345)&0x7fffffff;return s/0x7fffffff;}var a=new Array(BINS).fill(0);for(var i=0;i<1000;i++){var v=(r()+r()+r())/3;a[Math.min(BINS-1,Math.floor(v*BINS))]++;}REF=a.map(function(c){return c/1000;});})();function mount(el,cfg){cfg=cfg||{};var shift=0;function current(){var a=new Array(BINS).fill(0);var s=6;function r(){s=(s*1103515245+12345)&0x7fffffff;return s/0x7fffffff;}for(var i=0;i<1000;i++){var v=(r()+r()+r())/3+shift*0.04;v=Math.max(0,Math.min(0.999,v));a[Math.floor(v*BINS)]++;}return a.map(function(c){return c/1000;});} function psi(ref,cur){var s=0;for(var i=0;iweeks since launch '+''+'
    '+ u.runnable(rcode(),{label:'Compute the population-stability index in R'});var plot=el.querySelector('.dm-plot'),read=el.querySelector('.dm-read'),slider=el.querySelector('.dm-s'),wEl=el.querySelector('.dm-w');function draw(){var cur=current(),W=420,H=150,bw=W/BINS,mx=0.32;var svg='';REF.forEach(function(c,i){var h=c/mx*(H-16);svg+='';});cur.forEach(function(c,i){var h=c/mx*(H-16);svg+='';});svg+='';plot.innerHTML=svg;var ps=psi(REF,cur),alert=ps>=0.2;read.innerHTML='PSI '+ps.toFixed(2)+' · dashed = training reference, solid = live traffic. '+ (alert?'Drift alert (PSI ≥ 0.2) - the live inputs no longer match training. Retrain.':ps>=0.1?'Minor drift - keep watching.':'Stable - inputs still look like the training data.');wEl.textContent=shift.toFixed(1);} slider.addEventListener('input',function(){shift=+slider.value;draw();});draw();} function rcode(){return['# PSI compares a live sample to the training reference, bin by bin. >= 0.2 = real drift.','set.seed(1)','ref <- rnorm(1000, 0, 1) # training distribution','live <- rnorm(1000, 0.6, 1) # months later: shifted','br <- quantile(ref, probs = seq(0, 1, 0.1))','e <- table(cut(ref, br)) / length(ref)','o <- table(cut(live, br)) / length(live)','psi <- sum((o - e) * log(pmax(o, 1e-4) / pmax(e, 1e-4)))','psi # compare to 0.1 (watch) and 0.2 (alert)'].join('\n');} window.LessonWidgets.register('drift-monitor',mount);})();;(function(){'use strict';function mount(el,cfg){var u=window.LessonWidgets.u;if(!u)return;cfg=cfg||{};var data=cfg.data||[{x:1.4,y:0.2,facet:'setosa'},{x:1.3,y:0.2,facet:'setosa'},{x:1.5,y:0.1,facet:'setosa'},{x:1.4,y:0.3,facet:'setosa'},{x:4.7,y:1.4,facet:'versicolor'},{x:4.5,y:1.5,facet:'versicolor'},{x:4.9,y:1.5,facet:'versicolor'},{x:4.0,y:1.3,facet:'versicolor'},{x:6.0,y:2.5,facet:'virginica'},{x:5.9,y:2.1,facet:'virginica'},{x:6.1,y:2.3,facet:'virginica'},{x:5.6,y:1.8,facet:'virginica'}];var geom=cfg.geom||'point',xlab=cfg.x||'petal length',ylab=cfg.y||'petal width',fvar=cfg.facetVar||'species';var facets=[];data.forEach(function(d){if(facets.indexOf(d.facet)<0)facets.push(d.facet);});var xc=(xlab||'x').replace(/\s+/g,'_'),yc=(ylab||'y').replace(/\s+/g,'_'),fc=(fvar||'group').replace(/\s+/g,'_');function fgCode(mode){var base='library(ggplot2)\n'+u.rdf(data,[{name:xc,key:'x'},{name:yc,key:'y'},{name:fc,key:'facet'}])+'\n\n';return mode==='one'?base+'ggplot(df, aes('+xc+', '+yc+', color = '+fc+')) +\n geom_point()':base+'ggplot(df, aes('+xc+', '+yc+')) +\n geom_point() +\n facet_wrap(~ '+fc+')';} function fgSVG(mode){if(mode==='one')return u.plot(data,{geom:geom,x:xlab,y:ylab,w:440,h:260});var h='
    ';facets.forEach(function(f){var sub=data.filter(function(d){return d.facet===f;});h+='
    '+u.esc(f)+'
    '+u.plot(sub,{geom:geom,x:xlab,y:ylab,w:220,h:170})+'
    ';});return h+'
    ';} var wrap=document.createElement('div');wrap.style.cssText='font-family:IBM Plex Sans,system-ui,sans-serif';wrap.innerHTML='
    ';var segHost=wrap.querySelector('.fg-seg');segHost.innerHTML=u.seg([{v:'one',label:'One chart'},{v:'facet',label:'facet_wrap(~'+fvar+')'}],'one');var codeEl=wrap.querySelector('.fg-code');function render(mode){codeEl.innerHTML=u.runnable(fgCode(mode),{label:'Run this chart'});var po=codeEl.querySelector('.webr-plot-output');if(po){po.innerHTML=u.previewSeed(fgSVG(mode));po.classList.add('has-content');}} u.wireSeg(segHost,function(v){render(v);});render('one');el.innerHTML='';el.appendChild(wrap);} if(window.LessonWidgets)window.LessonWidgets.register('facet-grid',mount);})();;(function(){'use strict';var u=window.LessonWidgets.u,P=u.P;var G={A:{sel:0.46,tpr:0.82,fpr:0.20},B:{sel:0.28,tpr:0.66,fpr:0.19}};var DEFS={parity:{keys:['sel'],labels:['selection rate'],say:'Demographic parity asks for equal selection rates. Here A is picked 46% vs B 28% - a clear gap.'},opportunity:{keys:['tpr'],labels:['true-positive rate'],say:'Equal opportunity asks for equal true-positive rates among the truly qualified. A 0.82 vs B 0.66 - the model finds qualified A\'s more often.'},odds:{keys:['tpr','fpr'],labels:['true-positive rate','false-positive rate'],say:'Equalised odds needs BOTH rates equal. FPRs match (~0.20) but TPRs do not - so this fails too.'}};function mount(el,cfg){cfg=cfg||{};var def='parity';el.style.cssText='border:1px solid '+P.line+';border-radius:12px;background:#fff;padding:16px 17px';el.innerHTML='
    '+'
    '+'
    '+ u.runnable(rcode(),{label:'Compute per-group rates in R'});var seg=el.querySelector('.fm-seg'),barsEl=el.querySelector('.fm-bars'),read=el.querySelector('.fm-read');[['parity','demographic parity'],['opportunity','equal opportunity'],['odds','equalised odds']].forEach(function(o){var b=document.createElement('button');b.textContent=o[1];b.style.cssText='font:600 12px IBM Plex Sans,sans-serif;border:1px solid '+P.line+';border-radius:8px;padding:6px 11px;cursor:pointer';b.addEventListener('click',function(){def=o[0];draw();});seg.appendChild(b);});function group(metricKey,lab){var a=G.A[metricKey],b=G.B[metricKey],gap=Math.abs(a-b);function row(name,v,col){return'
    '+name+''+v.toFixed(2)+'
    ';} return'
    '+lab+(gap>0.05?' gap '+gap.toFixed(2)+'':' matched')+'
    '+row('group A',a,P.c0)+row('group B',b,P.acc)+'
    ';} function draw(){seg.querySelectorAll('button').forEach(function(x,i){var on=['parity','opportunity','odds'][i]===def;x.style.background=on?P.ink:'#fff';x.style.color=on?'#fff':P.body;});var d=DEFS[def];barsEl.innerHTML=d.keys.map(function(k,i){return group(k,d.labels[i]);}).join('');read.innerHTML=d.say;} draw();} function rcode(){return['# Fairness compares the SAME model across groups. Compute the rates per group.','set.seed(1); n <- 400','grp <- sample(c("A","B"), n, replace = TRUE)','true <- rbinom(n, 1, ifelse(grp == "A", 0.5, 0.4))','pred <- rbinom(n, 1, ifelse(grp == "A", 0.46, 0.28)) # model selects A more','','rate <- function(g) c(',' selection = mean(pred[grp == g]),',' TPR = mean(pred[grp == g & true == 1]),',' FPR = mean(pred[grp == g & true == 0]))','rbind(A = rate("A"), B = rate("B")) # compare the rows'].join('\n');} window.LessonWidgets.register('fairness-metrics',mount);})();;(function(){'use strict';var C0='#2563a8',C1='#b5631a';function mulberry32(a){return function(){a|=0;a=a+0x6D2B79F5|0;var t=Math.imul(a^a>>>15,1|a);t=t+Math.imul(t^t>>>7,61|t)^t;return((t^t>>>14)>>>0)/4294967296;};} function mount(el,cfg){cfg=cfg||{};var SEED=cfg.seed||7,MIN=cfg.min||1,MAX=cfg.max||80,START=cfg.start||1;var labels=cfg.labels||{c0:'stays',c1:'churns'};var rnd=mulberry32(SEED);function gauss(mx,my,s){var u=rnd(),v=rnd();var g=Math.sqrt(-2*Math.log(u+1e-9))*Math.cos(6.2832*v),g2=Math.sqrt(-2*Math.log(u+1e-9))*Math.sin(6.2832*v);return{x:Math.min(.97,Math.max(.03,mx+g*s)),y:Math.min(.97,Math.max(.03,my+g2*s))};} var DATA=[];(function(){function blob(mx,my,c,n){for(var k=0;k=pts.length-c0)?0:1;if(depth>=maxD||pts.length<4||c0===0||c0===pts.length)return{leaf:true,cls:maj};var axes=[rng()<0.5?'x':'y'],best=null;axes.forEach(function(ax){var vals=pts.map(function(p){return p[ax];}).sort(function(a,b){return a-b;});for(var v=0;v0;s--){var j=(rng()*(s+1))|0;var tmp=idx[s];idx[s]=idx[j];idx[j]=tmp;} var votes=[];for(var i2=0;i2=0.5?1:0)===TEST[i4].c)c++;}E[n]+=c/TEST.length;}} for(var n5=1;n5<=80;n5++)E[n5]/=K;for(var n6=2;n6<=80;n6++)if(E[n6]'+''+'
    '+'
    '+labels.c0+''+labels.c1+'
    '+'
    .
    Forest test accuracy
    '+'
    .
    A typical single tree
    '+'
    ';var slider=el.querySelector('.lw-slider'),fval=el.querySelector('.lw-fval'),canvas=el.querySelector('.lw-canvas');var forestEl=el.querySelector('.lw-forest'),oneEl=el.querySelector('.lw-one'),noteEl=el.querySelector('.lw-note');var ctx=canvas.getContext('2d');function render(){var N=+slider.value;fval.textContent=N;var W=canvas.width,H=canvas.height,cell=6;for(var px=0;pxOne tree. Hard, jagged regions, this is the overfitter from Lesson 1. Now add more.';else if(N<12)noteEl.innerHTML=''+N+' trees. The boundary is already softening as the votes blend, and test accuracy is climbing.';else noteEl.innerHTML=''+N+' trees. The boundary is smooth and the forest ('+(fA*100).toFixed(0)+'%) clearly beats a typical single tree ('+(oA*100).toFixed(0)+'%). It has converged.';} slider.addEventListener('input',render);render();} if(window.LessonWidgets)window.LessonWidgets.register('forest-averaging',mount);})();;(function(){'use strict';var C_STAY='#2563a8',C_CHURN='#b5631a',INK='#131720',MUT='#677084',LINE='#c5cdda';function gini(a,b){var n=a+b;if(!n)return 0;var p=a/n,q=b/n;return 1-p*p-q*q;} function f2(x){return(Math.round(x*100)/100).toFixed(2);} function dots(cx,cy,stay,churn){var n=stay+churn,per=10,gap=12,out='',i;var cols=Math.min(n,per),w=(cols-1)*gap,x0=cx-w/2;for(i=0;i';} return out;} function nodeBox(cx,top,w,h){return'';} function txt(x,y,s,col,fs,weight){return''+s+'';} function mount(el,cfg){cfg=cfg||{};var feature=cfg.feature||'tenure under 8 mo?';var P=cfg.parent||[10,10],L=cfg.left||[2,8],R=cfg.right||[8,2];var ln=cfg.labels||{},sName=ln.stay||'stays',cName=ln.churn||'churns';var gp=gini(P[0],P[1]),gl=gini(L[0],L[1]),gr=gini(R[0],R[1]);var nL=L[0]+L[1],nR=R[0]+R[1],nT=nL+nR;var gw=nT?(nL/nT)*gl+(nR/nT)*gr:0;var svg=''+ nodeBox(230,8,220,60)+dots(230,28,P[0],P[1])+txt(230,60,'parent: Gini = '+f2(gp),MUT,11)+''+''+ txt(230,92,feature,INK,11)+txt(158,116,'yes',MUT,10)+txt(302,116,'no',MUT,10)+ nodeBox(120,150,170,58)+dots(120,170,L[0],L[1])+txt(120,200,'Gini = '+f2(gl),MUT,11)+ nodeBox(340,150,170,58)+dots(340,170,R[0],R[1])+txt(340,200,'Gini = '+f2(gr),MUT,11)+ txt(230,250,'weighted Gini = '+f2(gw),INK,14,700)+ txt(230,272,'down from '+f2(gp)+': a purer split','#1f7a55',11)+'';el.innerHTML='
    '+'
    '+' '+sName+'   '+cName+'
    '+ svg+'
    ';} if(window.LessonWidgets)window.LessonWidgets.register('gini-split',mount);})();;(function(){'use strict';var u=window.LessonWidgets.u,P=u.P;var FAM={gamma:{lo:0,hi:8,dens:function(x){var k=2.2,th=1.1;return x<=0?0:Math.pow(x,k-1)*Math.exp(-x/th)/(Math.pow(th,k)*gammaf(k));},say:'Gamma fits a strictly positive, right-skewed outcome: spend, claim size, time-to-complete. Mean and variance rise together, and it never predicts a negative value.',use:'spend, claim amount, duration'},beta:{lo:0,hi:1,dens:function(x){var a=2.5,b=4;return(x<=0||x>=1)?0:Math.pow(x,a-1)*Math.pow(1-x,b-1)/betaf(a,b);},say:'Beta fits a proportion or rate bounded in [0, 1]: conversion rate, share of budget, defect fraction. A normal model would happily predict impossible values below 0 or above 1.',use:'proportion, rate, fraction'},tweedie:{lo:0,hi:8,dens:null,say:'Tweedie fits a spike of exact zeros plus a positive continuous tail: total insurance loss (most customers claim nothing; some claim a lot). No zero-free family (Gamma) can place mass exactly at 0.',use:'insurance loss, total sales with many zeros'}};function gammaf(z){var g=7,c=[0.99999999999980993,676.5203681218851,-1259.1392167224028,771.32342877765313,-176.61502916214059,12.507343278686905,-0.13857109526572012,9.9843695780195716e-6,1.5056327351493116e-7];if(z<0.5)return Math.PI/(Math.sin(Math.PI*z)*gammaf(1-z));z-=1;var x=c[0];for(var i=1;i'+'
    '+'
    '+ u.runnable(rcode(),{label:'The density shapes in base R'});var plot=el.querySelector('.gf-plot'),read=el.querySelector('.gf-read');var W=440,H=210,m={l:30,r:12,t:12,b:26},iw=W-m.l-m.r,ih=H-m.t-m.b;function draw(){var f=FAM[fam],svg='';svg+='';function sx(x){return m.l+(x-f.lo)/(f.hi-f.lo)*iw;} if(fam==='tweedie'){var pk=0;var tw=function(x){return x<=0?0:0.9*Math.pow(x,1.2)*Math.exp(-x/1.0);};for(var xi=0;xi<=f.hi;xi+=0.05)pk=Math.max(pk,tw(xi));var syT=function(v){return m.t+ih-v/(pk*1.15)*ih;};svg+='';svg+='P(loss = 0)';var tp='';for(var x2=0.05;x2<=f.hi;x2+=0.05)tp+=sx(x2).toFixed(1)+','+syT(tw(x2)).toFixed(1)+' ';svg+='';}else{var peak=0;for(var s=f.lo;s<=f.hi;s+=(f.hi-f.lo)/200)peak=Math.max(peak,f.dens(s));var sy=function(v){return m.t+ih-v/(peak*1.12)*ih;};var pts='',area=sx(f.lo).toFixed(1)+','+sy(0)+' ';for(var i=0;i<=200;i++){var x=f.lo+(f.hi-f.lo)*i/200;var yy=sy(f.dens(x));pts+=sx(x).toFixed(1)+','+yy.toFixed(1)+' ';area+=sx(x).toFixed(1)+','+yy.toFixed(1)+' ';} area+=sx(f.hi).toFixed(1)+','+sy(0);svg+='';} [f.lo,(f.lo+f.hi)/2,f.hi].forEach(function(t){svg+=''+t+'';});svg+='outcome value ('+f.use+')';svg+='';plot.innerHTML=svg;read.innerHTML=f.say;} u.wireSeg(el.querySelector('.gf-seg'),function(v){fam=v;draw();});draw();} function rcode(){return['# The outcome\'s support and skew pick the family. See the shapes:','x_pos <- seq(0.01, 8, 0.02)','x_prp <- seq(0.001, 0.999, 0.002)','','gamma_d <- dgamma(x_pos, shape = 2.2, scale = 1.1) # positive, right-skewed','beta_d <- dbeta(x_prp, 2.5, 4) # bounded in [0, 1]','c(gamma_support = "x > 0, skewed -> spend, claims",',' beta_support = "0 < x < 1 -> proportions",',' tweedie = "mass at 0 + positive tail -> insurance loss")','c(gamma_peak_at = round(x_pos[which.max(gamma_d)], 2),',' beta_peak_at = round(x_prp[which.max(beta_d)], 2))'].join('\n');} window.LessonWidgets.register('glm-family-shapes',mount);})();;(function(){'use strict';var u=window.LessonWidgets.u,P=u.P;var A=[3.3,4.2],B=[6.4,5.6],SD=1.25;var PTS=(function(){var s=4,out=[];function r(){s=(s*1103515245+12345)&0x7fffffff;return s/0x7fffffff;}function g(){return(r()+r()+r()-1.5)*1.3;}for(var i=0;i<22;i++)out.push([A[0]+g(),A[1]+g()]);for(var j=0;j<22;j++)out.push([B[0]+g(),B[1]+g()]);return out;})();function mount(el,cfg){cfg=cfg||{};var soft=true;function resp(p){var da=Math.exp(-((p[0]-A[0])*(p[0]-A[0])+(p[1]-A[1])*(p[1]-A[1]))/(2*SD*SD));var db=Math.exp(-((p[0]-B[0])*(p[0]-B[0])+(p[1]-B[1])*(p[1]-B[1]))/(2*SD*SD));return da/(da+db);} function mix(t){var c0=[32,86,210],c1=[201,162,74];return'rgb('+Math.round(c0[0]*t+c1[0]*(1-t))+','+Math.round(c0[1]*t+c1[1]*(1-t))+','+Math.round(c0[2]*t+c1[2]*(1-t))+')';} el.style.cssText='border:1px solid '+P.line+';border-radius:12px;background:#fff;padding:16px 17px';el.innerHTML='
    '+'
    '+'
    '+ u.runnable(rcode(),{label:'Compute mixture responsibilities in R'});var seg=el.querySelector('.gm-seg'),plot=el.querySelector('.gm-plot'),read=el.querySelector('.gm-read');[['true','soft (GMM)'],['false','hard (k-means)']].forEach(function(o){var b=document.createElement('button');b.textContent=o[1];b.style.cssText='font:600 12px IBM Plex Sans,sans-serif;border:1px solid '+P.line+';border-radius:8px;padding:6px 12px;cursor:pointer';b.addEventListener('click',function(){soft=o[0]==='true';draw();});seg.appendChild(b);});function draw(){seg.querySelectorAll('button').forEach(function(x,i){var on=['true','false'][i]===String(soft);x.style.background=on?P.ink:'#fff';x.style.color=on?'#fff':P.body;});var S=240,m=10,sc=(S-2*m)/10;function px(x){return m+x*sc;}function py(y){return(S-m)-y*sc;} var svg='';svg+='';[A,B].forEach(function(c){svg+='';});PTS.forEach(function(p){var t=resp(p);var col=soft?mix(t):(t>=0.5?P.acc:(P.c2||'#c9a24a'));svg+='';});svg+='';plot.innerHTML=svg;read.innerHTML=soft?'Soft: points near the overlap take an in-between colour - the model reports, say, 60% / 40% rather than forcing a side. That honesty is the point of a mixture model.':'Hard: every point is forced fully into one cluster, even the ambiguous ones in the overlap. k-means cannot express "maybe".';} draw();} function rcode(){return['# A 2-component mixture gives each point a PROBABILITY per cluster (a responsibility),','# not a hard label. Here it is by hand; the mclust package fits it for real.','set.seed(1)','x <- c(rnorm(40, -2), rnorm(40, 2))','mu <- c(-2, 2); sg <- c(1, 1) # two components','la <- dnorm(x, mu[1], sg[1]); lb <- dnorm(x, mu[2], sg[2])','resp <- la / (la + lb) # P(component 1 | x)','head(round(resp, 2), 10) # values near 0.5 = uncertain'].join('\n');} window.LessonWidgets.register('gmm-clusters',mount);})();;(function(){'use strict';var u=window.LessonWidgets.u,P=u.P;var XTR=[-4,-3,-1,0,2,3];var YTR=[0.820,-0.123,-0.925,0.160,0.942,0.059];function rbf(a,b,l){var d=a-b;return Math.exp(-d*d/(2*l*l));} function inv(M){var n=M.length,A=M.map(function(r,i){return r.concat(Array.from({length:n},function(_,j){return i===j?1:0;}));});for(var c=0;c'+'
    '+'
    '+ u.runnable(rcode(),{label:'The exact GP posterior mean and sd, from the RBF kernel in base R'});var plot=el.querySelector('.gp-plot'),read=el.querySelector('.gp-read');var W=340,H=240,RX=[-5.4,5.4],RY=[-2.2,2.2];function px(x){return(x-RX[0])/(RX[1]-RX[0])*W;} function py(y){return H-(y-RY[0])/(RY[1]-RY[0])*H;} function draw(){var f=posterior(l,sig),N=80,top=[],bot=[],mid=[];for(var i=0;i<=N;i++){var x=RX[0]+(RX[1]-RX[0])*i/N,r=f(x);top.push([px(x),py(r.m+1.96*r.sd)]);bot.push([px(x),py(r.m-1.96*r.sd)]);mid.push([px(x),py(r.m)]);} var band='M'+top.map(function(p){return p[0].toFixed(1)+','+p[1].toFixed(1);}).join(' L')+' L'+bot.slice().reverse().map(function(p){return p[0].toFixed(1)+','+p[1].toFixed(1);}).join(' L')+' Z';var line='M'+mid.map(function(p){return p[0].toFixed(1)+','+p[1].toFixed(1);}).join(' L');var dots=XTR.map(function(xt,i){return'';}).join('');var sdNear=f(-3).sd,sdFar=f(5).sd;plot.innerHTML=''+''+''+dots+'';read.innerHTML='Lengthscale '+l.toFixed(1)+': the band is tight near the points (sd ≈ '+sdNear.toFixed(2)+') and flares out where there is no data (sd ≈ '+sdFar.toFixed(2)+'). '+(l<0.7?'Short lengthscale → the mean wiggles and forgets fast between points.':l>1.5?'Long lengthscale → a smooth, stiff mean that may miss local bumps.':'A medium lengthscale balances the two.');} u.wireSeg(el.querySelector('.gp-seg'),function(v){l=parseFloat(v);draw();});draw();} function rcode(){return['# A Gaussian process posterior, from scratch: mean + uncertainty, no packages.','xtr <- c(-4, -3, -1, 0, 2, 3)','set.seed(1); ytr <- sin(xtr) + rnorm(6, 0, 0.1) # noisy observations','rbf <- function(a, b, l) exp(-outer(a, b, "-")^2 / (2 * l^2))','l <- 1.0; sig <- 0.1 # lengthscale, noise sd','K <- rbf(xtr, xtr, l) + sig^2 * diag(length(xtr))','xte <- seq(-5, 5, length.out = 6)','Ks <- rbf(xte, xtr, l); Kss <- rbf(xte, xte, l)','mu <- as.numeric(Ks %*% solve(K, ytr)) # posterior mean','sd <- sqrt(pmax(diag(Kss - Ks %*% solve(K, t(Ks))), 0)) # predictive sd','round(data.frame(x = xte, mean = mu, sd = sd), 2)','# sd is small next to a training point and grows out at the edges (-5, 5):','# that widening band is the GP telling you where it has no evidence.'].join('\n');} window.LessonWidgets.register('gp-posterior',mount);})();;(function(){'use strict';var u=window.LessonWidgets.u,P=u.P;var Z=[0.3,-0.4,0.1,0.5,-0.2,0.35,-0.5,0.2,-0.1,0.4,-0.3,0.15,0.45,-0.25,0.05,-0.45,0.25,-0.15,0.5,-0.35,0.1,0.3,-0.2,0.4,-0.4,0.2,-0.05,0.35,-0.3,0.15];function mount(el,cfg){cfg=cfg||{};var MAX=16,lr=(cfg.lr!=null)?+cfg.lr:0.3,round=Math.min(MAX,Math.max(0,+cfg.rounds||0));var N=Z.length,X=[],Y=[];for(var i=0;iboosting rounds (trees): '+''+'
    '+ u.runnable(rcode(lr),{label:'Boost it for real with rpart stumps in R'});var chart=el.querySelector('.gb-chart'),read=el.querySelector('.gb-read'),slider=el.querySelector('.gb-s'),rEl=el.querySelector('.gb-r');function draw(){var pred=PRED[round];var W=480,H=260,m2={t:14,r:14,b:32,l:40},iw=W-m2.l-m2.r,ih=H-m2.t-m2.b;var ylo=Math.min.apply(null,Y)-0.6,yhi=Math.max.apply(null,Y)+0.6;function sx(x){return m2.l+x/10*iw;}function sy(y){return m2.t+ih-(y-ylo)/(yhi-ylo)*ih;} var svg='';svg+='';svg+='';for(var k=0;k';svg+='';for(k=0;k';svg+='';chart.innerHTML=svg;var rmse=Math.sqrt(Y.reduce(function(a,y,k3){return a+(y-pred[k3])*(y-pred[k3]);},0)/N);read.innerHTML=(round===0?'Round 0 is just the mean - one flat line, big residuals.':'After '+round+' round'+(round===1?'':'s')+', each tree has nudged the fit toward the points it still got wrong.')+' The orange stems are the residuals; watch them shrink. RMSE '+rmse.toFixed(3)+'';rEl.textContent=round;} slider.addEventListener('input',function(){round=+slider.value;draw();});draw();} function rcode(lr){return['# Gradient boosting by hand: each shallow tree fits the CURRENT residuals,','# and we add a shrunken slice of it. Watch RMSE fall as trees stack.','library(rpart)','set.seed(1)','n <- 80','x <- sort(runif(n, 0, 10))','y <- sin(x) + 0.18 * x + rnorm(n, 0, 0.3)','','pred <- rep(mean(y), n) # start with one flat guess','lr <- '+lr,'for (m in 1:15) {',' resid <- y - pred # what we still get wrong',' stump <- rpart(resid ~ x, control = rpart.control(maxdepth = 1, cp = 0))',' pred <- pred + lr * predict(stump) # add a shrunken correction','}','plot(x, y, pch = 19, col = "gray70"); lines(x, pred, col = "forestgreen", lwd = 2)','sqrt(mean((y - pred)^2)) # RMSE after boosting'].join('\n');} window.LessonWidgets.register('gradient-boosting',mount);})();;(function(){'use strict';var u=window.LessonWidgets.u,P=u.P;function mount(el,cfg){cfg=cfg||{};var WSTAR=(cfg.min!=null)?+cfg.min:3;var W0=(cfg.start!=null)?+cfg.start:-2;var lr=0.1;function loss(w){return(w-WSTAR)*(w-WSTAR);} function grad(w){return 2*(w-WSTAR);} var path=[W0];el.style.cssText='border:1px solid '+P.line+';border-radius:12px;background:#fff;padding:16px 17px';el.innerHTML='
    '+''+'
    '+''+''+''+'
    '+'
    '+ u.runnable(rcode(WSTAR,W0),{label:'Run the descent loop in R'});var chart=el.querySelector('.gd-chart'),read=el.querySelector('.gd-read'),lrs=el.querySelector('.gd-lrs'),lrEl=el.querySelector('.gd-lr');function draw(){var W=480,H=250,m={t:14,r:14,b:34,l:42},iw=W-m.l-m.r,ih=H-m.t-m.b;var wlo=WSTAR-6.5,whi=WSTAR+6.5,lmax=loss(wlo)*1.05;function sx(w){return m.l+(w-wlo)/(whi-wlo)*iw;} function sy(L){return m.t+ih-Math.min(L,lmax)/lmax*ih;} var curve='';for(var w=wlo;w<=whi+0.001;w+=(whi-wlo)/80)curve+=sx(w).toFixed(1)+','+sy(loss(w)).toFixed(1)+' ';var svg='';svg+='';svg+='';svg+='';for(var i=0;i0)svg+='';var last=i===path.length-1;svg+='';} svg+='minimum';svg+='loss';svg+='';chart.innerHTML=svg;var curW=path[path.length-1],factor=Math.abs(1-2*lr);var state=factor>=1.001?['Diverging',P.bad,'The step overshoots so hard the ball climbs out of the bowl. Lower the learning rate.']:lr>0.5?['Oscillating in',P.c1,'The step is large: the ball bounces side to side but still settles. Faster, but jumpy.']:['Converging',P.acc,'Small, steady steps walk smoothly down to the minimum. Slow but safe.'];read.innerHTML=''+state[0]+'. '+state[2]+' step '+(path.length-1)+' · w '+curW.toFixed(2)+' · loss '+loss(curW).toFixed(2)+'';lrEl.textContent=lr.toFixed(2);} function step(){var w=path[path.length-1];path.push(w-lr*grad(w));if(path.length>40)path.shift();draw();} lrs.addEventListener('input',function(){lr=+lrs.value;path=[W0];draw();});el.querySelector('.gd-step').addEventListener('click',step);el.querySelector('.gd-run').addEventListener('click',function(){for(var k=0;k<15;k++)step();});el.querySelector('.gd-reset').addEventListener('click',function(){path=[W0];draw();});draw();} function rcode(wstar,w0){return['# Gradient descent: nudge w downhill by -learning_rate * gradient, over and over.','# Loss L(w) = (w - '+wstar+')^2, so the gradient is 2 * (w - '+wstar+').','grad <- function(w) 2 * (w - '+wstar+')','','lr <- 0.10 # try 0.5, then 0.95, then 1.10 and watch it blow up','w <- '+w0,'for (i in 1:20) {',' w <- w - lr * grad(w)',' cat(sprintf("step %2d: w = %7.3f loss = %7.3f\\n", i, w, (w - '+wstar+')^2))','}'].join('\n');} window.LessonWidgets.register('gradient-descent',mount);})();;(function(){'use strict';var u=window.LessonWidgets.u,P=u.P;function mount(el,cfg){cfg=cfg||{};var HR=2;el.style.cssText='border:1px solid '+P.line+';border-radius:12px;background:#fff;padding:16px 17px';el.innerHTML='
    '+u.seg([{v:'0.5',label:'HR 0.5'},{v:'1',label:'HR 1'},{v:'2',label:'HR 2'},{v:'3',label:'HR 3'}],'2')+'
    '+'
    '+'
    '+ u.runnable(rcode(),{label:'A Cox proportional-hazards fit, and its hazard ratios'});var plot=el.querySelector('.hr-plot'),read=el.querySelector('.hr-read');var W=340,H=240,TMAX=60,r0=0.035;function px(t){return(t/TMAX)*(W-8)+4;} function py(s){return H-s*(H-14)-7;} function s0(t){return Math.exp(-r0*t);} function curvePath(pw){var p='',N=80;for(var i=0;i<=N;i++){var t=TMAX*i/N,s=Math.pow(s0(t),pw);p+=(i?' L':'M')+px(t).toFixed(1)+','+py(s).toFixed(1);} return p;} function draw(){var half='';var med0=Math.log(2)/r0,med1=Math.log(2)/(r0*HR);plot.innerHTML=''+ half+''+''+'S(t)'+'time';read.innerHTML='Dashed grey is the baseline group; the solid line is S(t) = S0(t)'+HR.toFixed(1)+'. '+ (HR>1?'HR '+HR.toFixed(1)+' > 1: '+HR.toFixed(1)+'× the hazard, so the curve drops faster and median survival falls from '+med0.toFixed(0)+' to '+med1.toFixed(0)+'.':HR<1?'HR '+HR.toFixed(1)+' < 1: protective, half the hazard, so median survival rises from '+med0.toFixed(0)+' to '+med1.toFixed(0)+'.':'HR = 1: identical hazard, the curves coincide.')+' Under proportional hazards the two curves never cross.';} u.wireSeg(el.querySelector('.hr-seg'),function(v){HR=parseFloat(v);draw();});draw();} function rcode(){return['# Cox proportional hazards: read each coefficient as a hazard ratio.','library(survival)','set.seed(2)','n <- 150','age <- round(runif(n, 40, 80))','smoker <- rbinom(n, 1, 0.4)','lp <- 0.03 * (age - 60) + 0.8 * smoker # the true log-hazard','event_t <- rexp(n, 0.02 * exp(lp)); cens_t <- runif(n, 0, 60)','time <- pmin(event_t, cens_t); status <- as.integer(event_t <= cens_t)','','cox <- coxph(Surv(time, status) ~ age + smoker)','round(exp(coef(cox)), 3) # hazard ratios','# smoker ~ 1.6x the hazard, and each extra year of age ~ 1.04x;','# a hazard ratio scales the whole survival curve, S1(t) = S0(t)^HR.'].join('\n');} window.LessonWidgets.register('hazard-ratio',mount);})();;(function(){'use strict';var u=window.LessonWidgets.u,P=u.P;function rng(seed){var s=seed;return function(){s=(s*1103515245+12345)&0x7fffffff;return s/0x7fffffff;};} var r=rng(5);var MAJ=[],MIN=[];for(var i=0;i<60;i++)MAJ.push([2+r()*5,2+r()*5]);for(var j=0;j<8;j++)MIN.push([5.5+r()*2.5,5.5+r()*2.5]);function mount(el,cfg){cfg=cfg||{};var mode='orig';el.style.cssText='border:1px solid '+P.line+';border-radius:12px;background:#fff;padding:16px 17px';el.innerHTML='
    '+'
    '+'
    '+ u.runnable(rcode(),{label:'Count classes and upsample the minority in R'});var seg=el.querySelector('.ir-seg'),plot=el.querySelector('.ir-plot'),read=el.querySelector('.ir-read');[['orig','original'],['over','oversample'],['smote','SMOTE']].forEach(function(o){var b=document.createElement('button');b.textContent=o[1];b.style.cssText='font:600 12px IBM Plex Sans,sans-serif;border:1px solid '+P.line+';border-radius:8px;padding:6px 12px;cursor:pointer';b.addEventListener('click',function(){mode=o[0];draw();});seg.appendChild(b);});function minoritySet(){if(mode==='orig')return{pts:MIN,synth:[]};if(mode==='over'){var d=[];for(var k=0;k<52;k++)d.push(MIN[k%MIN.length]);return{pts:MIN,synth:d};} var sy=[],rr=rng(3);for(var k=0;k<52;k++){var a=MIN[Math.floor(rr()*MIN.length)],b=MIN[Math.floor(rr()*MIN.length)],t=rr();sy.push([a[0]+(b[0]-a[0])*t,a[1]+(b[1]-a[1])*t]);}return{pts:MIN,synth:sy};} function draw(){seg.querySelectorAll('button').forEach(function(x,i){var on=['orig','over','smote'][i]===mode;x.style.background=on?P.ink:'#fff';x.style.color=on?'#fff':P.body;});var S=240,m=10,sc=(S-2*m)/10;function px(x){return m+x*sc;}function py(y){return(S-m)-y*sc;} var svg='';svg+='';MAJ.forEach(function(p){svg+='';});var ms=minoritySet();ms.synth.forEach(function(p){svg+='';});ms.pts.forEach(function(p){svg+='';});svg+='';plot.innerHTML=svg;var minN=MIN.length+ms.synth.length;read.innerHTML='Majority '+MAJ.length+' · minority '+minN+'. '+ (mode==='orig'?'A model trained here just predicts the majority and ignores the rare class.':mode==='over'?'Oversampling duplicates minority rows - balanced counts, but the duplicates add no new information and can overfit.':'SMOTE synthesises new minority points between real neighbours - balanced AND more varied, but never let it touch the test fold.');} draw();} function rcode(){return['# Imbalance: the model can score 95% accuracy by always predicting the majority.','set.seed(1)','y <- factor(c(rep("no", 950), rep("yes", 50))) # 95% / 5%','table(y)','','# simplest fix: upsample the minority to match the majority','idx_min <- which(y == "yes")','up <- sample(idx_min, sum(y == "no"), replace = TRUE)','table(y[c(which(y == "no"), up)]) # now balanced (SMOTE via the themis package goes further)'].join('\n');} window.LessonWidgets.register('imbalance-resample',mount);})();;(function(){'use strict';var INK='#131720',MUT='#677084',BAR='#1f7a55',TOP='#2563a8';function mount(el,cfg){cfg=cfg||{};var items=(cfg.items&&cfg.items.length)?cfg.items.slice():[{label:'tenure',value:100},{label:'monthly spend',value:71},{label:'total spend',value:58},{label:'support calls',value:34},{label:'contract type',value:22},{label:'age',value:12}];items.sort(function(a,b){return b.value-a.value;});var max=items[0].value||1,n=items.length;var rowH=30,padT=12,padB=8,labW=120,barX=labW+8,barMax=280;var H=padT+n*rowH+padB,W=barX+barMax+46,rows='',i;for(i=0;i'+items[i].label+''+''+''+items[i].value+'';} var svg=''+rows+'';el.innerHTML='
    '+svg+'
    ';} if(window.LessonWidgets)window.LessonWidgets.register('importance-bars',mount);})();;(function(){'use strict';var u=window.LessonWidgets.u,P=u.P;function rng(seed){var s=seed>>>0;return function(){s=(s*1664525+1013904223)>>>0;return s/4294967296;};} function gauss(r){return Math.sqrt(-2*Math.log(Math.max(r(),1e-9)))*Math.cos(6.2831853*r());} function build(){var r=rng(7),pts=[];for(var i=0;i<70;i++)pts.push([0.5+0.075*gauss(r),0.52+0.075*gauss(r)]);pts.push([0.9,0.12]);return pts;} function isoPath(pts,pIdx,seed){var r=rng(seed),box=[0,1,0,1],idx=pts.map(function(_,i){return i;}),cuts=[],guard=0;while(idx.length>1&&guard++<40){var j=r()<0.5?0:1;var vals=idx.map(function(i){return pts[i][j];});var lo=Math.min.apply(null,vals),hi=Math.max.apply(null,vals);if(hi-lo<1e-6)break;var sp=lo+(hi-lo)*r();cuts.push({axis:j,at:sp,box:box.slice()});var pv=pts[pIdx][j],keepLow=pv'+'
    '+'
    '+ u.runnable(rcode(),{label:'Isolation forest from scratch: average path length -> anomaly score, base R'});var plot=el.querySelector('.if-plot'),read=el.querySelector('.if-read');var W=340,H=260,PAD=14;function sx(x){return PAD+x*(W-2*PAD);} function sy(y){return H-PAD-y*(H-2*PAD);} function avgLen(pIdx){var s=0,N=60;for(var t=0;t';return'';}).join('');var dots=pts.map(function(p,i){var isSel=i===pIdx,isOut=i===outlier;return'';}).join('');plot.innerHTML=''+''+ lines+dots+''+path.len+' cuts to isolate';read.innerHTML='This '+(sel==='outlier'?'outlier':'cluster point')+' is fenced off on its own after '+path.len+' random cuts here, and '+avg.toFixed(1)+' on average over many trees, giving an anomaly score of '+score.toFixed(2)+'. '+ (sel==='outlier'?'Out in empty space, a few cuts suffice, so the short path scores near 1: anomalous.':'Buried among neighbours, it takes many cuts, so the long path scores near 0.5: normal.');} u.wireSeg(el.querySelector('.if-seg'),function(v){sel=v;draw();});draw();} function rcode(){return['# Isolation Forest from scratch: anomalies isolate in fewer random splits (shorter paths). Base R.','set.seed(1)','X <- rbind(matrix(rnorm(200), 100, 2), c(4.5, 4.5)) # 100 normal points + 1 anomaly (row 101)','cn <- function(n) if (n > 2) 2*(log(n-1)+0.5772157) - 2*(n-1)/n else if (n==2) 1 else 0 # avg path in a random tree','ipath <- function(x, d, e = 0) { # depth of random splits to isolate x within d',' n <- nrow(d)',' if (n <= 1 || e > 30) return(e + cn(n))',' j <- sample(ncol(d), 1); lo <- min(d[,j]); hi <- max(d[,j])',' if (lo == hi) return(e + cn(n))',' sp <- runif(1, lo, hi) # random split on a random feature',' side <- if (x[j] < sp) d[,j] < sp else d[,j] >= sp',' ipath(x, d[side, , drop = FALSE], e + 1) # recurse into the side holding x','}','score <- function(i, trees = 100) # anomaly score = 2^(-avg path / cn)',' 2^(-mean(replicate(trees, ipath(X[i,], X))) / cn(nrow(X)))','round(c(normal_pt = score(1), anomaly_pt = score(101)), 3)','#> normal_pt anomaly_pt','#> 0.382 0.834'].join('\n');} window.LessonWidgets.register('isolation-forest',mount);})();;(function(){'use strict';var u=window.LessonWidgets.u,P=u.P;function rng(seed){var s=seed>>>0;return function(){s=(s*1664525+1013904223)>>>0;return s/4294967296;};} var STR={strong:0.8,weak:0.12};function build(zcoef){var r=rng(3),spare=null;function rnorm(){if(spare!==null){var v=spare;spare=null;return v;}var a=Math.max(r(),1e-12),b=r(),m=Math.sqrt(-2*Math.log(a));spare=m*Math.sin(2*Math.PI*b);return m*Math.cos(2*Math.PI*b);} var z=[],x=[],y=[];for(var i=0;i<400;i++){var zi=rnorm(),ui=rnorm();var xi=zcoef*zi+1.2*ui+rnorm();var yi=2*xi+3*ui+rnorm();z.push(zi);x.push(xi);y.push(yi);} return{z:z,x:x,y:y};} function mean(a){return a.reduce(function(s,v){return s+v;},0)/a.length;} function cov(a,b){var ma=mean(a),mb=mean(b),s=0;for(var i=0;i'+'
    '+'
    '+ u.runnable(rcode(),{label:'Two-stage least squares (IV) by hand, base R'});var plot=el.querySelector('.iv-plot'),read=el.querySelector('.iv-read');var W=340,H=200,PAD=10;function draw(){var d=build(STR[strength]);var mx=mean(d.x),my=mean(d.y);var naive=cov(d.x,d.y)/cov(d.x,d.x);var iv=cov(d.z,d.y)/cov(d.z,d.x);var fstrength=cov(d.z,d.x)*cov(d.z,d.x)/(cov(d.z,d.z)*cov(d.x,d.x))*(d.x.length-2)/Math.max(1e-6,1-cov(d.z,d.x)*cov(d.z,d.x)/(cov(d.z,d.z)*cov(d.x,d.x)));var xr=[Math.min.apply(null,d.x),Math.max.apply(null,d.x)],yr=[Math.min.apply(null,d.y),Math.max.apply(null,d.y)];function px(x){return PAD+(x-xr[0])/(xr[1]-xr[0])*(W-2*PAD);} function py(y){return H-22-(y-yr[0])/(yr[1]-yr[0])*(H-32);} var dots=d.x.map(function(xi,i){return'';}).join('');function seg(slope,col,dash){var x1=xr[0],x2=xr[1];var y1=my+slope*(x1-mx),y2=my+slope*(x2-mx);return'';} plot.innerHTML=''+ dots+seg(naive,P.bad)+seg(iv,P.acc,true)+'naive OLS '+naive.toFixed(2)+''+'IV '+iv.toFixed(2)+''+'treatment x -> outcome y';read.innerHTML='Naive OLS reads a slope of '+naive.toFixed(2)+', biased by the hidden confounder; IV recovers '+iv.toFixed(2)+' (true effect 2) from the instrument-driven part of x. '+ (strength==='weak'?'First-stage F ≈ '+fstrength.toFixed(0)+': a weak instrument barely moves x, so the Wald ratio\'s denominator is tiny and the estimate is unstable (rule of thumb: need F > 10).':'First-stage F ≈ '+fstrength.toFixed(0)+', comfortably above the F > 10 rule: a strong instrument gives a stable estimate.');} u.wireSeg(el.querySelector('.iv-seg'),function(v){strength=v;draw();});draw();} function rcode(){return['# Instrumental variables (2SLS): recover a causal slope despite an unobserved confounder. Base R.','set.seed(3)','n <- 1000','z <- rnorm(n) # instrument (random encouragement)','u <- rnorm(n) # UNOBSERVED confounder','x <- 0.8 * z + 1.2 * u + rnorm(n) # treatment: driven by z AND u','y <- 2 * x + 3 * u + rnorm(n) # TRUE effect of x is 2; u confounds','','naive <- coef(lm(y ~ x))["x"] # biased upward by u','xhat <- fitted(lm(x ~ z)) # stage 1: the exogenous, z-driven part of x','iv <- coef(lm(y ~ xhat))["xhat"] # stage 2','round(c(true = 2, naive_ols = unname(naive), iv_2sls = unname(iv)), 2)','#> true naive_ols iv_2sls','#> 2.00 3.16 1.85','','summary(lm(x ~ z))$fstatistic[1] # first-stage strength (need F > 10)','#> value','#> 257.8'].join('\n');} window.LessonWidgets.register('iv-2stage',mount);})();;(function(){'use strict';var OPS=[{v:'inner',label:'inner'},{v:'left',label:'left'},{v:'right',label:'right'},{v:'full',label:'full'},{v:'semi',label:'semi'},{v:'anti',label:'anti'}];var WHY={inner:'inner_join keeps only rows whose key is in BOTH tables.',left:'left_join keeps every LEFT row; unmatched right columns become NA.',right:'right_join keeps every RIGHT row; unmatched left columns become NA.',full:'full_join keeps every row from BOTH; gaps become NA.',semi:'semi_join keeps LEFT rows that HAVE a match (left columns only).',anti:'anti_join keeps LEFT rows with NO match (left columns only).'};var OPFN={inner:'inner_join',left:'left_join',right:'right_join',full:'full_join',semi:'semi_join',anti:'anti_join'};function mount(el,cfg){var u=window.LessonWidgets.u;if(!u)return;cfg=cfg||{};var L=cfg.left||{cols:['id','name','dept'],rows:[[1,'Aarti','S'],[2,'Biju','S'],[3,'Chen','E'],[4,'Devi','X']]};var R=cfg.right||{cols:['dept','dept_name'],rows:[['S','Sales'],['E','Eng'],['M','Marketing']]};var key=cfg.key||'dept',op=cfg.op||'inner';var li=L.cols.indexOf(key),ri=R.cols.indexOf(key);var rMap={};R.rows.forEach(function(r){rMap[r[ri]]=r;});var lKeys={};L.rows.forEach(function(r){lKeys[r[li]]=1;});var rExtra=R.cols.filter(function(c){return c!==key;});function compute(o){var cols,rows=[];if(o==='semi'||o==='anti'){cols=L.cols.slice();L.rows.forEach(function(r){var has=rMap[r[li]]!=null;if((o==='semi')===has)rows.push(r.slice());});return{cols:cols,rows:rows};} cols=L.cols.concat(rExtra);function joinRow(lr,rr){var row=lr?lr.slice():L.cols.map(function(c){return c===key?(rr?rr[ri]:null):null;});rExtra.forEach(function(c){row.push(rr?rr[R.cols.indexOf(c)]:null);});return row;} if(o==='inner')L.rows.forEach(function(lr){if(rMap[lr[li]])rows.push(joinRow(lr,rMap[lr[li]]));});else if(o==='left')L.rows.forEach(function(lr){rows.push(joinRow(lr,rMap[lr[li]]||null));});else if(o==='right')R.rows.forEach(function(rr){var lr=L.rows.find(function(x){return x[li]===rr[ri];});rows.push(joinRow(lr||null,rr));});else if(o==='full'){L.rows.forEach(function(lr){rows.push(joinRow(lr,rMap[lr[li]]||null));});R.rows.forEach(function(rr){if(!lKeys[rr[ri]])rows.push(joinRow(null,rr));});} return{cols:cols,rows:rows};} var wrap=document.createElement('div');wrap.style.cssText='font-family:IBM Plex Sans,system-ui,sans-serif';var matchTintL={},matchTintR={};L.rows.forEach(function(r,i){if(rMap[r[li]])matchTintL[i+','+li]=u.P.add;});R.rows.forEach(function(r,i){if(lKeys[r[ri]])matchTintR[i+','+ri]=u.P.add;});wrap.innerHTML='
    '+'
    LEFT (x)
    '+u.tbl(L.cols,L.rows,{hi:matchTintL})+'
    '+'
    RIGHT (y)
    '+u.tbl(R.cols,R.rows,{hi:matchTintR})+'
    '+'
    '+'
    join by '+u.esc(key)+':
    '+'
    '+'
    '+'
    ';var segHost=wrap.querySelector('.jd-seg');segHost.innerHTML=u.seg(OPS,op);var res=wrap.querySelector('.jd-res'),cap=wrap.querySelector('.jd-cap'),codeEl=wrap.querySelector('.jd-code');function jdCode(o){return'library(dplyr)\n'+u.rdfCR('x',L.cols,L.rows)+'\n'+u.rdfCR('y',R.cols,R.rows)+'\n\n'+OPFN[o]+'(x, y, by = "'+key+'")';} function render(o){var r=compute(o);res.innerHTML=u.tbl(r.cols,r.rows);cap.innerHTML=WHY[o]+' '+r.rows.length+' row'+(r.rows.length===1?'':'s')+'.';codeEl.innerHTML=u.runnable(jdCode(o),{label:'Run this join'});} u.wireSeg(segHost,function(v){render(v);});render(op);el.innerHTML='';el.appendChild(wrap);} if(window.LessonWidgets)window.LessonWidgets.register('join-diagram',mount);})();;(function(){'use strict';var u=window.LessonWidgets.u,P=u.P;var PTS=(function(){var a=[],s=11;function r(){s=(s*1103515245+12345)&0x7fffffff;return s/0x7fffffff;} for(var i=0;i<26;i++){var ang=r()*6.283,rad=r()*1.1;a.push({x:rad*Math.cos(ang),y:rad*Math.sin(ang),c:1});} for(var j=0;j<34;j++){var a2=r()*6.283,rr=2.1+r()*1.0;a.push({x:rr*Math.cos(a2),y:rr*Math.sin(a2),c:0});} return a;})();function score(kernel,x,y){var rad2=x*x+y*y;if(kernel==='linear')return 0.9*x+0.4*y+0.15;if(kernel==='poly')return 3.2-rad2;return 1.6*Math.exp(-rad2/1.4)-0.5;} function isSV(kernel,p){return Math.abs(score(kernel,p.x,p.y))<(kernel==='linear'?0.8:0.9);} function mount(el,cfg){cfg=cfg||{};var kernel='linear';el.style.cssText='border:1px solid '+P.line+';border-radius:12px;background:#fff;padding:16px 17px';el.innerHTML='
    '+u.seg([{v:'linear',label:'Linear'},{v:'poly',label:'Polynomial'},{v:'rbf',label:'RBF'}],'linear')+'
    '+'
    '+'
    '+ u.runnable(rcode(),{label:'Three kernels with e1071::svm(), and their training error'});var plot=el.querySelector('.ks-plot'),read=el.querySelector('.ks-read');var W=300,H=300,R=[-3.4,3.4];function px(x){return(x-R[0])/(R[1]-R[0])*W;} function py(y){return H-(y-R[0])/(R[1]-R[0])*H;} function draw(){var cells='',step=12;for(var gx=0;gx';} var err=0,dots='';PTS.forEach(function(p){var pred=score(kernel,p.x,p.y)>0?1:0;if(pred!==p.c)err++;var sv=isSV(kernel,p);dots+='';});plot.innerHTML=''+cells+dots+'';read.innerHTML=kernel==='linear'?'Linear kernel: '+err+' of '+PTS.length+' misclassified. No straight boundary can wrap the inner class, so a linear SVM is stuck.':''+(kernel==='poly'?'Polynomial':'RBF')+' kernel: '+err+' misclassified. The kernel trick lifts the points into a higher space where a flat boundary exists; back in 2-D it appears as a curve. The circled points are the support vectors that define it.';} u.wireSeg(el.querySelector('.ks-seg'),function(v){kernel=v;draw();});draw();} function rcode(){return['# Two classes no straight line can split: an inner blob inside a ring.','library(e1071)','set.seed(11)','ang <- runif(60, 0, 2*pi)','rad <- c(runif(26, 0, 1.1), 2.1 + runif(34, 0, 1)) # inner vs ring','d <- data.frame(x = rad*cos(ang), y = rad*sin(ang),',' cls = factor(c(rep(1,26), rep(0,34))))','','err <- function(k) mean(predict(svm(cls ~ x + y, d, kernel = k)) != d$cls)','c(linear = err("linear"), polynomial = err("polynomial"), radial = err("radial"))','# linear can never separate the ring; radial (RBF) drives training error to ~0.'].join('\n');} window.LessonWidgets.register('kernel-svm',mount);})();;(function(){'use strict';var u=window.LessonWidgets.u,P=u.P;var STD=[{t:1.5,e:1},{t:2.2,e:1},{t:3.1,e:1},{t:4.0,e:1},{t:4.6,e:0},{t:5.2,e:1},{t:6.0,e:1},{t:6.1,e:1},{t:7.4,e:1},{t:8.0,e:0},{t:9.2,e:1},{t:11.0,e:1},{t:13.5,e:1},{t:15.0,e:0},{t:18.0,e:1}];var NEW=[{t:3.0,e:0},{t:5.1,e:1},{t:7.2,e:1},{t:9.0,e:0},{t:10.4,e:1},{t:12.0,e:0},{t:13.6,e:1},{t:15.1,e:1},{t:16.0,e:0},{t:18.5,e:1},{t:20.0,e:0},{t:21.2,e:1},{t:22.0,e:0},{t:23.5,e:1},{t:24.0,e:0}];function km(data){var d=data.slice().sort(function(a,b){return a.t-b.t;});var n=d.length,S=1,steps=[{t:0,S:1}],cens=[];for(var i=0;i'+'
    '+'
    '+ u.runnable(rcode(),{label:'Kaplan-Meier curves and the log-rank test with survival::survfit'});var plot=el.querySelector('.km-plot'),read=el.querySelector('.km-read');var W=340,H=240,TMAX=25,PAD=4;function px(t){return(t/TMAX)*(W-PAD)+PAD;} function py(s){return H-s*(H-14)-7;} var kmS=km(STD),kmN=km(NEW);function stepPath(steps){var p='M'+px(0).toFixed(1)+','+py(1).toFixed(1),prev=1;steps.forEach(function(pt){p+=' L'+px(pt.t).toFixed(1)+','+py(prev).toFixed(1)+' L'+px(pt.t).toFixed(1)+','+py(pt.S).toFixed(1);prev=pt.S;});p+=' L'+px(TMAX).toFixed(1)+','+py(prev).toFixed(1);return p;} function curve(kmData,col,dim){var ticks=kmData.cens.map(function(c){return'';}).join('');return''+ticks;} function draw(){var half='';var showS=show==='both'||show==='standard',showN=show==='both'||show==='new';plot.innerHTML=''+ half+ curve(kmS,P.acc,!showS)+curve(kmN,P.c0,!showN)+'50% survival'+'S(t)'+'time (months)';read.innerHTML='Median survival is where a curve crosses 50%: standard ≈ 6 months, new drug ≈ 13.5 months. The vertical ticks are censored patients (still alive when last seen). The gap between the curves is what the log-rank test weighs, here p ≈ 0.0005.';} u.wireSeg(el.querySelector('.km-seg'),function(v){show=v;draw();});draw();} function rcode(){return['# Kaplan-Meier survival curves for two arms, and the log-rank test.','library(survival)','set.seed(1)','n <- 80','arm <- factor(rep(c("standard", "new"), each = 40))','lambda <- ifelse(arm == "new", 0.06, 0.12) # the new drug lowers the hazard','event_t <- rexp(n, lambda) # time to death','cens_t <- runif(n, 0, 24) # time to loss-to-follow-up','time <- pmin(event_t, cens_t)','status <- as.integer(event_t <= cens_t) # 1 = died, 0 = censored','','km <- survfit(Surv(time, status) ~ arm)','summary(km)$table[, c("records", "events", "median")] # median survival per arm','survdiff(Surv(time, status) ~ arm) # log-rank test of the gap','# the new arm has the higher median and the curves differ (log-rank p ~ 0.0005).'].join('\n');} window.LessonWidgets.register('km-curve',mount);})();;(function(){'use strict';var u=window.LessonWidgets.u,P=u.P;var COL=[P.c0,P.acc,(P.c2||'#c9a24a'),'#7a8a55'];var PTS=(function(){var seeds=[[2.4,2.4],[7.4,3.0],[4.6,7.2]],out=[],s=7;function rnd(){s=(s*1103515245+12345)&0x7fffffff;return s/0x7fffffff;} seeds.forEach(function(c){for(var i=0;i<9;i++)out.push([c[0]+(rnd()-0.5)*3,c[1]+(rnd()-0.5)*3]);});return out;})();function mount(el,cfg){cfg=cfg||{};var k=cfg.k||3;var cents=[[3.5,5.5],[5.5,3.0],[6.5,6.0]].slice(0,k);var assign=PTS.map(function(){return 0;}),phase='assign',iter=0;function dist(a,b){return(a[0]-b[0])*(a[0]-b[0])+(a[1]-b[1])*(a[1]-b[1]);} function wss(){var s=0;PTS.forEach(function(p,i){s+=dist(p,cents[assign[i]]);});return s;} el.style.cssText='border:1px solid '+P.line+';border-radius:12px;background:#fff;padding:16px 17px';el.innerHTML='
    '+'
    '+''+''+''+'
    '+ u.runnable(rcode(),{label:'Run k-means in R and read the clusters'});var plot=el.querySelector('.km-plot'),read=el.querySelector('.km-read');var S=250,m=14,sc=(S-2*m)/10;function px(x){return m+x*sc;}function py(y){return(S-m)-y*sc;} function draw(){var svg='';svg+='';PTS.forEach(function(p,i){svg+='';});cents.forEach(function(c,j){svg+='';});svg+='';plot.innerHTML=svg;read.textContent='iteration '+iter+' · next: '+phase+' · within-SS '+wss().toFixed(1);} function step(){if(phase==='assign'){PTS.forEach(function(p,i){var best=0,bd=Infinity;cents.forEach(function(c,j){var d=dist(p,c);if(dneighbors k = '+''+'
    '+ u.runnable(rcode(),{label:'Do the k-NN vote by hand in R'});var chart=el.querySelector('.kn-chart'),read=el.querySelector('.kn-read'),slider=el.querySelector('.kn-s'),kEl=el.querySelector('.kn-k');var W=460,H=300,m={t:14,r:14,b:14,l:14},iw=W-m.l-m.r,ih=H-m.t-m.b;function sx(x){return m.l+x/10*iw;} function sy(y){return m.t+ih-y/10*ih;} function draw(){var d=PTS.map(function(p,i){return{i:i,dist:Math.sqrt((p.x-qx)*(p.x-qx)+(p.y-qy)*(p.y-qy))};}).sort(function(a,b){return a.dist-b.dist;});var nn=d.slice(0,k),votes={A:0,B:0};nn.forEach(function(e){votes[PTS[e.i].c]++;});var pred=votes.A>=votes.B?'A':'B',isIn={};nn.forEach(function(e){isIn[e.i]=1;});var svg='';nn.forEach(function(e){svg+='';});PTS.forEach(function(p,i){var col=p.c==='A'?CA:CB;svg+='';});var qc=pred==='A'?CA:CB;svg+='';svg+='';chart.innerHTML=svg;read.innerHTML='k = '+k+': '+votes.A+' class A, '+votes.B+' class B → predict class '+pred+'. '+ (k===1?'At k=1 the query just copies its single closest point - jumpy and noise-sensitive.':'Larger k averages more neighbors: a smoother, steadier boundary, but it can blur real detail.')+' Click the box to move the query.';kEl.textContent=k;} chart.addEventListener('click',function(e){var svg=chart.querySelector('svg');if(!svg)return;var pt=svg.createSVGPoint();pt.x=e.clientX;pt.y=e.clientY;var loc=pt.matrixTransform(svg.getScreenCTM().inverse());qx=Math.max(0,Math.min(10,(loc.x-m.l)/iw*10));qy=Math.max(0,Math.min(10,(1-(loc.y-m.t)/ih)*10));draw();});slider.addEventListener('input',function(){k=+slider.value;draw();});draw();} function rcode(){return['# k-NN with no package: measure distance to every training point, take the','# k closest, and let them vote.','train <- data.frame(',' x = c(2,3,2.5,4,3,1.5,4.5,3.5,2, 7,8,6.5,8,7,6,8.5,7.5,6),',' y = c(3,2,4, 3,4,2.5,2, 3.5,5, 8,7,7.5,8.5,6,8,7,9, 6.5),',' cls = factor(rep(c("A","B"), each = 9)))','','query <- c(x = 5, y = 5)','k <- 5','dist <- sqrt((train$x - query["x"])^2 + (train$y - query["y"])^2)','nn <- order(dist)[1:k] # the k nearest rows','table(train$cls[nn]) # the votes','names(which.max(table(train$cls[nn]))) # predicted class'].join('\n');} window.LessonWidgets.register('knn-vote',mount);})();;(function(){'use strict';var u=window.LessonWidgets.u,P=u.P;function mount(el,cfg){cfg=cfg||{};var R=Math.max(10,+cfg.rounds||40);function train(r){return 0.16+1.5/(0.5*r+1);} function valid(r){return 0.30+1.35/(0.5*r+1)+0.0135*r;} var DS=[];for(var r=1;r<=R;r++)DS.push({r:r,train:train(r),valid:valid(r)});var best=DS.reduce(function(b,d){return d.validstop after round '+''+'
    '+ u.runnable(rcode(),{label:'Track train vs validation per round in R'});var chart=el.querySelector('.lcv-chart'),read=el.querySelector('.lcv-read'),slider=el.querySelector('.lcv-s'),nEl=el.querySelector('.lcv-n');function draw(){var W=480,H=250,m={t:14,r:14,b:34,l:44},iw=W-m.l-m.r,ih=H-m.t-m.b;var ymax=Math.max.apply(null,DS.map(function(d){return d.valid;}))*1.06,ymin=Math.min.apply(null,DS.map(function(d){return d.train;}))*0.9;function sx(rr){return m.l+(rr-1)/(R-1)*iw;}function sy(v){return m.t+ih-(v-ymin)/(ymax-ymin)*ih;} function pl(key,col){return'';} var svg='';svg+='best';svg+=pl('valid',P.c1)+pl('train',P.acc);svg+='';svg+='';svg+='validationtrain';svg+='boosting rounds →';chart.innerHTML=svg;var s=stopbest+2?['Stopped too late.',P.bad,'Past the validation minimum, training error keeps dropping but validation creeps up - that gap is memorized noise. Overfitting.']:['Right around the sweet spot.',P.acc,'You stopped near the validation minimum: the most signal, the least overfit. This is what early stopping automates.'];read.innerHTML=''+s[0]+' '+s[2]+' train '+DS[stop-1].train.toFixed(2)+' · valid '+DS[stop-1].valid.toFixed(2)+'';nEl.textContent=stop;} slider.addEventListener('input',function(){stop=+slider.value;draw();});draw();} function rcode(){return['# Early stopping: boost while watching BOTH errors. Train keeps falling;','# validation bottoms out then rises. Stop at the validation minimum.','library(rpart)','set.seed(1)','n <- 160; x <- sort(runif(n, 0, 10)); y <- sin(x) + 0.18 * x + rnorm(n, 0, 0.4)','tr <- 1:110; te <- 111:n','pred <- rep(mean(y[tr]), n); lr <- 0.3','tr_err <- va_err <- numeric(40)','for (m in 1:40) {',' st <- rpart(r ~ x, data = data.frame(x = x[tr], r = y[tr] - pred[tr]),',' control = rpart.control(maxdepth = 1, cp = 0))',' pred <- pred + lr * predict(st, data.frame(x = x))',' tr_err[m] <- sqrt(mean((y[tr] - pred[tr])^2))',' va_err[m] <- sqrt(mean((y[te] - pred[te])^2))','}','plot(va_err, type = "l", col = "darkorange", lwd = 2, ylim = range(tr_err, va_err),',' xlab = "round", ylab = "RMSE"); lines(tr_err, col = "forestgreen", lwd = 2)','which.min(va_err) # stop here'].join('\n');} window.LessonWidgets.register('learning-curve',mount);})();;(function(){'use strict';var u=window.LessonWidgets.u,P=u.P;function mount(el,cfg){cfg=cfg||{};var base=[{x:1,y:1.6},{x:2,y:2.0},{x:3,y:2.6},{x:4,y:2.9},{x:5,y:3.6},{x:6,y:3.9},{x:7,y:4.6}];var px=13;var py=3.0;function ols(pts){var n=pts.length,sx=0,sy=0,sxx=0,sxy=0;pts.forEach(function(p){sx+=p.x;sy+=p.y;sxx+=p.x*p.x;sxy+=p.x*p.y;});var b=(n*sxy-sx*sy)/(n*sxx-sx*sx);return{b:b,a:(sy-b*sx)/n};} el.style.cssText='border:1px solid '+P.line+';border-radius:12px;background:#fff;padding:16px 17px';el.innerHTML='
    '+''+'
    '+ u.runnable(rcode(base,px),{label:'Measure the influence in R (Cook’s distance)'});var chart=el.querySelector('.lv-chart'),read=el.querySelector('.lv-read'),slider=el.querySelector('.lv-s'),yEl=el.querySelector('.lv-y');function draw(){var all=base.concat([{x:px,y:py}]);var fitAll=ols(all),fitBase=ols(base);var W=480,H=270,m={t:14,r:14,b:34,l:40},iw=W-m.l-m.r,ih=H-m.t-m.b;var xlo=0,xhi=px+1,ylo=0,yhi=12;function sx(x){return m.l+(x-xlo)/(xhi-xlo)*iw;} function sy(y){return m.t+ih-(y-ylo)/(yhi-ylo)*ih;} function line(f,col,dash){return'';} var svg='';svg+='';svg+='';svg+=line(fitBase,P.faint,true)+line(fitAll,P.acc,false);base.forEach(function(p){svg+='';});svg+='';svg+='with the point';svg+='without it';svg+='';chart.innerHTML=svg;var dSlope=Math.abs(fitAll.b-fitBase.b),pull=Math.abs(py-(fitBase.a+fitBase.b*px));var sev=pull>5?['Highly influential.',P.bad]:pull>2?['Pulling the line.',P.c1]:['Barely budging it.',P.acc];read.innerHTML=''+sev[0]+' One far-out point at x='+px+' has high leverage: it sits far from the others on x, so the line swings to chase it. Slope with it '+''+fitAll.b.toFixed(2)+' vs without '+''+fitBase.b.toFixed(2)+'.';yEl.textContent=py.toFixed(1);} slider.addEventListener('input',function(){py=+slider.value;draw();});draw();} function rcode(base,px){var rows=base.concat([{x:px,y:11}]);var rdf=u.rdf(rows,[{name:'x',key:'x'},{name:'y',key:'y'}],'d');return['# The last row is a far-out, high-leverage point. Fit with and without it,','# and let Cook’s distance flag how much it moves the model.',rdf,'','fit_all <- lm(y ~ x, data = d)','fit_drop <- lm(y ~ x, data = d[-nrow(d), ]) # drop the influential point','rbind(with_point = coef(fit_all), without = coef(fit_drop))','','round(cooks.distance(fit_all), 3) # influence of each row','plot(d$x, d$y, pch = 19); abline(fit_all, col = "forestgreen", lwd = 2)','abline(fit_drop, col = "gray60", lwd = 2, lty = 2)'].join('\n');} window.LessonWidgets.register('leverage-point',mount);})();;(function(){'use strict';var u=window.LessonWidgets.u,P=u.P;function rng(seed){var s=seed>>>0;return function(){s=(s*1664525+1013904223)>>>0;return s/4294967296;};} function gauss(r){return Math.sqrt(-2*Math.log(Math.max(r(),1e-9)))*Math.cos(6.2831853*r());} function build(){var r=rng(11),pts=[],i;for(i=0;i<40;i++)pts.push([0.35+0.08*gauss(r),0.55+0.08*gauss(r)]);for(i=0;i<20;i++)pts.push([0.72+0.05*gauss(r),0.35+0.05*gauss(r)]);pts.push([0.6,0.8]);return pts;} function dist(a,b){var dx=a[0]-b[0],dy=a[1]-b[1];return Math.sqrt(dx*dx+dy*dy);} function lofAll(pts,k){var n=pts.length,D=[],i,j;for(i=0;i'+'
    '+'
    '+ u.runnable(rcode(),{label:'Local Outlier Factor from scratch: local density ratio, base R'});var plot=el.querySelector('.lf-plot'),read=el.querySelector('.lf-read');var W=340,H=250,PAD=14;function sx(x){return PAD+x*(W-2*PAD);} function sy(y){return H-PAD-y*(H-2*PAD);} function col(lof){return lof>1.8?P.bad:lof>1.3?'#e08a2e':P.c0;} function draw(){var lof=lofAll(pts,k);var dots=pts.map(function(p,i){var isOut=i===outlier;return'';}).join('');plot.innerHTML=''+''+ dots+'bigger, redder = higher LOF';var clusterMean=0,cc=0;lof.forEach(function(v,i){if(i!==outlier){clusterMean+=v;cc++;}});read.innerHTML='At k = '+k+', the highlighted point scores LOF = '+lof[outlier].toFixed(2)+', while ordinary cluster points sit near '+(clusterMean/cc).toFixed(2)+'. An LOF near 1 means "as dense as my neighbours"; well above 1 means "in a far sparser pocket than the points around me", the local signal a single global distance cutoff would miss.';} u.wireSeg(el.querySelector('.lf-seg'),function(v){k=parseInt(v,10);draw();});draw();} function rcode(){return['# Local Outlier Factor from scratch: an outlier is far less dense than its own neighbours. Base R.','set.seed(1)','X <- rbind(matrix(rnorm(100), 50, 2), c(3.5, 3.5)) # 50-point cluster + 1 outlier (row 51)','k <- 5; D <- as.matrix(dist(X))','knn <- function(i) order(D[i,])[2:(k+1)] # k nearest neighbours (drop self)','kdist <- function(i) D[i, knn(i)[k]] # distance to the k-th neighbour','rd <- function(i, o) max(D[i, o], kdist(o)) # reachability distance of i from o','lrd <- function(i) 1 / mean(sapply(knn(i), function(o) rd(i, o))) # local reachability density','lof <- function(i) mean(sapply(knn(i), lrd)) / lrd(i) # neighbour density / my density','round(c(cluster_pt = lof(1), outlier_pt = lof(51)), 2)','#> cluster_pt outlier_pt','#> 1.02 3.80'].join('\n');} window.LessonWidgets.register('lof-density',mount);})();;(function(){'use strict';var u=window.LessonWidgets.u,P=u.P;var XS=[1,2,2.5,3,4,4.5,5.5,6,7,7.5,8,9];var YS=[0,0,0,0,0,1,0,1,1,1,1,1];var B0=-5.5,B1=1.1;function prob(x){return 1/(1+Math.exp(-(B0+B1*x)));} function mount(el,cfg){cfg=cfg||{};var thr=0.5;el.style.cssText='border:1px solid '+P.line+';border-radius:12px;background:#fff;padding:16px 17px';el.innerHTML='
    '+''+'
    '+ u.runnable(rcode(),{label:'Fit the logistic model for real in R'});var chart=el.querySelector('.lc-chart'),read=el.querySelector('.lc-read'),slider=el.querySelector('.lc-s'),tEl=el.querySelector('.lc-t');function draw(){var W=480,H=268,m={t:16,r:14,b:36,l:44},iw=W-m.l-m.r,ih=H-m.t-m.b;var xlo=0,xhi=10;function sx(x){return m.l+(x-xlo)/(xhi-xlo)*iw;} function sy(p){return m.t+ih-p*ih;} var bound=(Math.log(thr/(1-thr))-B0)/B1;var svg='';svg+='';svg+='';[0,0.5,1].forEach(function(g){svg+=''+g+'';});var curve='';for(var x=xlo;x<=xhi+0.001;x+=0.1)curve+=sx(x).toFixed(1)+','+sy(prob(x)).toFixed(1)+' ';svg+='';svg+='';if(bound>xlo&&bound';var tp=0,fp=0,tn=0,fn=0;XS.forEach(function(x,i){var pred=prob(x)>=thr?1:0,act=YS[i],ok=pred===act;if(act===1&&pred===1)tp++;else if(act===0&&pred===1)fp++;else if(act===0&&pred===0)tn++;else fn++;svg+='';});svg+='feature x →';svg+='P(y = 1)';svg+='';chart.innerHTML=svg;read.innerHTML='Predict 1 when the curve is above the threshold. At '+thr.toFixed(2)+': '+fp+' false positive'+(fp===1?'':'s')+', '+fn+' false negative'+(fn===1?'':'s')+'. Lower the threshold to catch more 1s (fewer false negatives, more false positives); raise it for the reverse. The S-curve is fixed; only the cutoff moves.';tEl.textContent=thr.toFixed(2);} slider.addEventListener('input',function(){thr=+slider.value;draw();});draw();} function rcode(){return['# Logistic regression models a PROBABILITY with an S-curve, then a threshold','# turns that probability into a class.','x <- c(1, 2, 2.5, 3, 4, 4.5, 5.5, 6, 7, 7.5, 8, 9)','y <- c(0, 0, 0, 0, 0, 1, 0, 1, 1, 1, 1, 1)','fit <- glm(y ~ x, family = binomial)','coef(fit) # intercept and slope (log-odds)','','p <- predict(fit, type = "response") # fitted probabilities','plot(x, y, pch = 19, ylab = "P(y = 1)")','curve(predict(fit, data.frame(x = x), type = "response"),',' add = TRUE, col = "steelblue", lwd = 2)','table(predicted = as.integer(p >= 0.5), actual = y) # try 0.3 or 0.7'].join('\n');} window.LessonWidgets.register('logistic-curve',mount);})();;(function(){'use strict';var u=window.LessonWidgets.u,P=u.P;function rng(seed){var s=seed>>>0;return function(){s=(s*1664525+1013904223)>>>0;return s/4294967296;};} function plogis(z){return 1/(1+Math.exp(-z));} function build(){var r=rng(1),spare=null;function rnorm(){if(spare!==null){var v=spare;spare=null;return v;}var a=Math.max(r(),1e-12),b=r(),m=Math.sqrt(-2*Math.log(a));spare=m*Math.sin(2*Math.PI*b);return m*Math.cos(2*Math.PI*b);} var trt=[],ctrl=[];for(var i=0;i<600;i++){var x=rnorm(),ps=plogis(0.8*x);if(r()'+'
    '+'
    '+ u.runnable(rcode(),{label:'Propensity score + nearest-neighbour matching, base R'});var plot=el.querySelector('.mo-plot'),read=el.querySelector('.mo-read');var NB=20,W=340,H=190,MID=H/2,bw=(W-8)/NB;function draw(){var control=phase==='before'?d.ctrl:d.matched;var ht=hist(d.trt,NB),hc=hist(control,NB);var mx=Math.max(Math.max.apply(null,ht),Math.max.apply(null,hc))||1;var sc=(MID-16)/mx;function bars(h,up){return h.map(function(c,k){var hh=c*sc;var y=up?MID-hh:MID;return'';}).join('');} var s=smd(d.trt,control);plot.innerHTML=''+ bars(ht,true)+bars(hc,false)+''+'treated'+'control'+'propensity score ->'+'';read.innerHTML=phase==='before'?'Treated units pile up at high propensity scores and controls at low ones: standardized mean difference '+s.toFixed(2)+'. A raw outcome gap here is confounded.':'Each treated unit is paired with its nearest-propensity control, so the two histograms now line up: standardized mean difference '+s.toFixed(2)+' (under 0.1 is good balance). The matched comparison is fair.';} u.wireSeg(el.querySelector('.mo-seg'),function(v){phase=v;draw();});draw();} function rcode(){return['# Confounded treatment: recover the true effect with propensity-score matching. Base R.','set.seed(1)','n <- 600; x <- rnorm(n)','treat <- rbinom(n, 1, plogis(0.8 * x)) # treatment depends on x (a confounder)','y <- 2 * treat + 1.5 * x + rnorm(n) # the TRUE treatment effect is 2','','naive <- mean(y[treat == 1]) - mean(y[treat == 0]) # biased: ignores x','ps <- glm(treat ~ x, family = binomial)$fitted.values','ctrl <- which(treat == 0); trt <- which(treat == 1)','match <- sapply(trt, function(i) ctrl[which.min(abs(ps[ctrl] - ps[i]))])','att <- mean(y[trt] - y[match]) # matched difference','round(c(true = 2, naive = naive, matched = att), 2)','#> true naive matched','#> 2.00 3.02 2.01','','smd <- function(a, b) (mean(a) - mean(b)) / sqrt((var(a) + var(b)) / 2)','round(c(before = smd(x[trt], x[ctrl]), after = smd(x[trt], x[match])), 2) # covariate balance','#> before after','#> 0.74 0.01'].join('\n');} window.LessonWidgets.register('matching-overlap',mount);})();;(function(){'use strict';var u=window.LessonWidgets.u,P=u.P;var MU0=3.19,SD0=0.224;function logpost(x){var z=(x-MU0)/SD0;return-0.5*z*z;} function rng(seed){var s=seed>>>0;return function(){s=(s*1664525+1013904223)>>>0;return s/4294967296;};} function run(propSD,n){var r=rng(7),spare=null;function rnorm(){if(spare!==null){var v=spare;spare=null;return v;}var u1=Math.max(r(),1e-12),u2=r(),m=Math.sqrt(-2*Math.log(u1));spare=m*Math.sin(2*Math.PI*u2);return m*Math.cos(2*Math.PI*u2);} var x=0,acc=0,chain=[x];for(var i=1;i'+'
    '+'
    '+ u.runnable(rcode(),{label:'A Metropolis sampler for a normal-mean posterior, from scratch in base R'});var plot=el.querySelector('.mc-plot'),read=el.querySelector('.mc-read');var W=340,Ht=130,Hh=96,YR=[2.3,4.1];function tx(i){return(i/(N-1))*(W-4)+2;} function ty(v){return Ht-(v-YR[0])/(YR[1]-YR[0])*(Ht-8)-4;} function draw(){var res=run(propSD,N);var trace=res.chain.map(function(v,i){return tx(i).toFixed(1)+','+ty(v).toFixed(1);});var burn=Math.floor(N*0.2),bins=30,lo=YR[0],hi=YR[1],cnt=new Array(bins).fill(0);for(var i=burn;i=0&&b';}).join('');var curve=[];for(var g=0;g<=60;g++){var xv=lo+(hi-lo)*g/60,d=Math.exp(logpost(xv));curve.push(((xv-lo)/(hi-lo)*W).toFixed(1)+','+(Hh-d*(Hh-10)).toFixed(1));} plot.innerHTML=''+''+'trace (chain over iterations)'+''+ bars+''+'samples vs true posterior';read.innerHTML='Proposal sd '+propSD.toFixed(2)+', acceptance '+(res.acc*100).toFixed(0)+'%. '+ (propSD<0.2?'Tiny steps: almost every move is accepted, but the chain crawls and barely explores, poor mixing.':propSD>1.5?'Huge steps: most proposals land in low-probability regions and are rejected, so the trace sticks in flat runs.':'A medium step mixes well: acceptance near 40-50%, the trace roams freely, and the histogram fills in the true posterior.');} u.wireSeg(el.querySelector('.mc-seg'),function(v){propSD=parseFloat(v);draw();});draw();} function rcode(){return['# Metropolis sampling: explore a posterior with a guided random walk, base R.','set.seed(1)','y <- rnorm(20, 3, 1) # data: 20 obs, unknown mean, sd = 1','logpost <- function(mu) sum(dnorm(y, mu, 1, log = TRUE)) # flat prior on mu','','metropolis <- function(start, prop_sd, n) {',' x <- numeric(n); x[1] <- start; acc <- 0',' for (i in 2:n) {',' cand <- rnorm(1, x[i-1], prop_sd) # propose a step',' if (log(runif(1)) < logpost(cand) - logpost(x[i-1])) { # accept by the Metropolis rule',' x[i] <- cand; acc <- acc + 1',' } else x[i] <- x[i-1] # else stay put',' }',' list(chain = x, acc = acc / (n-1))','}','fit <- metropolis(0, 0.5, 5000)','burn <- 1:1000 # discard the warm-up','round(c(post_mean = mean(fit$chain[-burn]), post_sd = sd(fit$chain[-burn]),',' accept = fit$acc), 3)','# the chain recovers the analytic posterior: mean ~ mean(y), sd ~ 1/sqrt(20).'].join('\n');} window.LessonWidgets.register('mcmc-walk',mount);})();;(function(){'use strict';var u=window.LessonWidgets.u,P=u.P;function mulberry32(a){return function(){a|=0;a=a+0x6D2B79F5|0;var t=Math.imul(a^a>>>15,1|a);t=t+Math.imul(t^t>>>7,61|t)^t;return((t^t>>>14)>>>0)/4294967296;};} var LABEL={none:'No correction',bonferroni:'Bonferroni',holm:'Holm'};var HALO='stroke="#fff" stroke-width="3" paint-order="stroke" stroke-linejoin="round"';function adjust(p,method){var k=p.length,i;if(method==='none')return p.slice();if(method==='bonferroni'){var b=[];for(i=0;i1?'
    '+ u.seg(modes.map(function(m){return{v:m,label:LABEL[m]};}),mode)+'
    ':'')+''+''+'
    '+'
    '+ u.runnable(rcode(alpha),{label:'The same simulation in R'});var plot=el.querySelector('.ms-plot'),read=el.querySelector('.ms-read'),klab=el.querySelector('.ms-klab'),slider=el.querySelector('.ms-k');var W=460,H=322;var m1={t:18,r:14,b:30,l:42},h1=150;var m2={t:h1+78,r:14,b:26,l:42},h2=54;function sxK(kv){return m1.l+(kv-1)/Math.max(1,kMax-1)*(W-m1.l-m1.r);} function syR(r){return m1.t+(1-r)*h1;} function sxP(pv){return m2.l+pv*(W-m2.l-m2.r);} function fmtPct(x){return(100*x).toFixed(1)+'%';} function draw(){var rate=curves[mode][k-1];var theory=1-Math.pow(1-alpha,k);var thresh=(mode==='none')?alpha:alpha/k;var g='';[0,0.25,0.5,0.75,1].forEach(function(r){g+=''+''+(100*r)+'%';});g+=''+'alpha = '+alpha+'';var th=[];for(var a=1;a<=kMax;a++)th.push(sxK(a).toFixed(1)+','+syR(1-Math.pow(1-alpha,a)).toFixed(1));g+='';var sim=[];for(var b2=1;b2<=kMax;b2++)sim.push(sxK(b2).toFixed(1)+','+syR(curves[mode][b2-1]).toFixed(1));g+='';g+=''+'';var lateK=k>kMax*0.62;g+=''+fmtPct(rate)+'';g+='';[1,Math.round(kMax/2),kMax].forEach(function(kv){g+=''+kv+'';});g+='k, number of tests in the family';g+='Studies with at least one false positive';var raw=[],ix;for(ix=0;ixOne of those studies: its '+k+' p-value'+(k===1?'':'s')+'';g+=''+'';for(ix=0;ix';} g+='';[0,0.25,0.5,0.75,1].forEach(function(pv){g+=''+pv+'';});g+=''+nHit+' called significant';plot.innerHTML=''+ g+'';klab.innerHTML='k = '+k+' test'+(k===1?'':'s')+' in the family';var thTxt='1 − (1 − '+alpha+')'+k+' = '+fmtPct(theory)+'';if(mode==='none'){read.innerHTML='Across '+nStudies.toLocaleString()+' simulated studies with no real effect anywhere, '+''+fmtPct(rate)+' produced at least one p-value below '+alpha+'. Every one of those is a false positive. The formula predicts '+thTxt+'.'+ (k===1?' With a single test the rate is just alpha, which is the risk you agreed to.':'');}else{read.innerHTML=LABEL[mode]+' judges each test against '+alpha+' / '+k+' = '+ (alpha/k).toPrecision(2)+' instead of '+alpha+'. That drops the share of studies with any false positive to '+''+fmtPct(rate)+', back at alpha, while the uncorrected rate (dashed) sits at '+fmtPct(theory)+'. '+'Holm and Bonferroni give the SAME number here: when nothing is real, both reject something only if the smallest p-value clears alpha / k.';}} if(modes.length>1)u.wireSeg(el.querySelector('.ms-seg'),function(v){mode=v;draw();});slider.addEventListener('input',function(){k=+slider.value;draw();});draw();} function rcode(alpha){return['# Run many studies in which NOTHING is real, and count how often at least one','# of the k tests comes back "significant". Change k and run it again.','set.seed(11)','alpha <- '+alpha,'k <- 14 # tests in the family','sims <- 4000 # simulated studies','','# Under a true null a p-value is Uniform(0, 1), so runif(k) IS k null tests.','any_hit <- function(k) {',' p <- runif(k)',' c(none = min(p) < alpha,',' bonferroni = min(p.adjust(p, method = "bonferroni")) < alpha,',' holm = min(p.adjust(p, method = "holm")) < alpha)','}','','round(colMeans(t(replicate(sims, any_hit(k)))), 3)','round(1 - (1 - alpha)^k, 3) # what the formula predicts for the uncorrected case'].join('\n');} window.LessonWidgets.register('multiplicity-sim',mount);})();;(function(){'use strict';function mount(el,cfg){var u=window.LessonWidgets.u;if(!u)return;cfg=cfg||{};var cols=cfg.cols||['region','month','sales'];var rows=cfg.rows||[['East','Jan',10],['East','Feb',14],['West','Jan',8],['West','Feb',12]];var nestBy=cfg.nestBy||cols[0];var listCol=cfg.listCol||'data';var view='flat';var dataCols=cols.filter(function(c){return c!==nestBy;});var bi=cols.indexOf(nestBy);var wrap=document.createElement('div');wrap.style.cssText='font-family:IBM Plex Sans,system-ui,sans-serif';function groups(){var order=[],by={};rows.forEach(function(r){var k=r[bi];if(!(k in by)){by[k]=0;order.push(k);}by[k]++;});return order.map(function(k){return{key:k,n:by[k]};});} function nestedTable(){var h='';h+=''+'';groups().forEach(function(g){h+=''+'';});return h+'
    '+u.esc(nestBy)+''+u.esc(listCol)+' (list-column)
    '+u.esc(g.key)+''+'tibble ['+g.n+' x '+dataCols.length+']'+'
    ';} function rCode(){var objs=rows.map(function(r){var o={};cols.forEach(function(c,i){o[c]=r[i];});return o;});var rcols=cols.map(function(c){return{name:c,key:c};});return'library(dplyr)\nlibrary(tidyr)\n\n'+ u.rdf(objs,rcols,'df')+'\n\n'+'nested <- df |> nest('+listCol+' = c('+dataCols.join(', ')+'))\n'+'nested # one row per '+nestBy+', '+listCol+' is a list of tibbles\n\n'+'nested |> unnest('+listCol+') # back to the flat frame';} function render(){var isFlat=view==='flat';wrap.querySelector('.nu-table').innerHTML=isFlat?u.tbl(cols,rows):nestedTable();wrap.querySelector('.nu-cap').innerHTML=isFlat?''+rows.length+' rows, one per observation. nest() packs the '+dataCols.join(' and ')+' columns into a list-column.':''+groups().length+' rows, one per '+nestBy+'. Each '+listCol+' cell holds a whole tibble. unnest() spreads it back out.';wrap.querySelector('.nu-run').innerHTML=u.runnable(rCode(),{label:'Run nest() and unnest()'});} wrap.innerHTML='
    '+'
    '+u.seg([{v:'flat',label:'Flat frame'},{v:'nested',label:'Nested'}],'flat')+'
    '+'
    '+'
    '+'
    '+'
    ';u.wireSeg(wrap.querySelector('.nu-seg'),function(v){view=v;render();});el.innerHTML='';el.appendChild(wrap);render();} if(window.LessonWidgets)window.LessonWidgets.register('nest-unnest',mount);})();;(function(){'use strict';var INK='#131720',MUT='#677084',ACC='#1f7a55',TAIL='#b5631a',LINE='#c5cdda';function erf(x){var s=x<0?-1:1;x=Math.abs(x);var t=1/(1+0.3275911*x);var y=1-(((((1.061405429*t-1.453152027)*t)+1.421413741)*t-0.284496736)*t+0.254829592)*t*Math.exp(-x*x);return s*y;} function Phi(x){return 0.5*(1+erf(x/Math.SQRT2));} function pdf(x){return Math.exp(-x*x/2)/Math.sqrt(2*Math.PI);} function mount(el,cfg){cfg=cfg||{};var tails=cfg.tails||2,MAX=cfg.max||4,start=cfg.start!=null?cfg.start:2.0,label=cfg.label||'observed t';var W=460,H=230,padB=30,padT=10,x0=18,x1=W-18,midY=H-padB;var sx=function(z){return x0+(z+MAX)/(2*MAX)*(x1-x0);};var peak=pdf(0),sy=function(d){return padT+(1-d/peak)*(midY-padT-6);};var curve='',i;for(i=0;i<=120;i++){var z=-MAX+i/120*2*MAX;curve+=(i?'L':'M')+sx(z).toFixed(1)+' '+sy(pdf(z)).toFixed(1)+' ';} el.innerHTML='
    '+label+''+start.toFixed(2)+'
    '+'
    '+'
    '+''+''+ (tails===2?'':'')+''+''+ (tails===2?'':'')+'0'+'
    '+'
    ';var slider=el.querySelector('.nd-slider'),tval=el.querySelector('.nd-tval'),lineR=el.querySelector('.nd-line-r'),lineL=el.querySelector('.nd-line-l'),tailR=el.querySelector('.nd-tail-r'),tailL=el.querySelector('.nd-tail-l'),out=el.querySelector('.nd-out');function tailPath(fromZ,dir){var p='M'+sx(fromZ).toFixed(1)+' '+midY+' ',n=40,k;for(k=0;k<=n;k++){var z=fromZ+(dir*MAX-fromZ)*k/n;p+='L'+sx(z).toFixed(1)+' '+sy(pdf(z)).toFixed(1)+' ';} return p+'L'+sx(dir*MAX).toFixed(1)+' '+midY+' Z';} function render(){var t=parseFloat(slider.value);tval.textContent=t.toFixed(2);var xr=sx(t);lineR.setAttribute('x1',xr);lineR.setAttribute('x2',xr);tailR.setAttribute('d',tailPath(t,1));if(tails===2){var xl=sx(-t);lineL.setAttribute('x1',xl);lineL.setAttribute('x2',xl);tailL.setAttribute('d',tailPath(-t,-1));} var p=(tails===2?2:1)*(1-Phi(t)),sig=p<0.05;out.innerHTML='p-value = '+p.toFixed(3)+'   '+(sig?'reject H0 (p < 0.05)':'fail to reject H0');} slider.addEventListener('input',render);render();} if(window.LessonWidgets)window.LessonWidgets.register('null-distribution',mount);})();;(function(){'use strict';var u=window.LessonWidgets.u,P=u.P;function mount(el,cfg){cfg=cfg||{};var pts=(cfg.points&&cfg.points.length)?cfg.points.map(function(p){return{x:+p.x,y:+p.y};}):[{x:1,y:2.1},{x:2,y:2.9},{x:3,y:3.7},{x:4,y:3.4},{x:5,y:5.2},{x:6,y:5.0},{x:7,y:6.6},{x:8,y:6.1},{x:9,y:7.8}];function ols(){var n=pts.length,sx=0,sy=0,sxx=0,sxy=0;pts.forEach(function(p){sx+=p.x;sy+=p.y;sxx+=p.x*p.x;sxy+=p.x*p.y;});var b=(n*sxy-sx*sy)/(n*sxx-sx*sx),a=(sy-b*sx)/n;return{a:a,b:b};} var best=ols();var b=+(best.b*0.45).toFixed(2);var a=+(best.a+1.2).toFixed(2);el.style.cssText='border:1px solid '+P.line+';border-radius:12px;background:#fff;padding:16px 17px';el.innerHTML='
    '+'
    '+''+''+'
    '+'
    '+'
    '+''+'
    '+ u.runnable(rcode(pts),{label:'Fit it for real: lm() finds this exact line'});var chart=el.querySelector('.of-chart'),sseEl=el.querySelector('.of-sse'),bs=el.querySelector('.of-bs'),as=el.querySelector('.of-as'),bEl=el.querySelector('.of-b'),aEl=el.querySelector('.of-a');function draw(){var W=480,H=280,m={t:14,r:14,b:34,l:40},iw=W-m.l-m.r,ih=H-m.t-m.b;var xs=pts.map(function(p){return p.x;}),ys=pts.map(function(p){return p.y;});var xlo=Math.min.apply(null,xs)-0.6,xhi=Math.max.apply(null,xs)+0.6;var ylo=Math.min.apply(null,ys)-1.4,yhi=Math.max.apply(null,ys)+1.4;function sx(x){return m.l+(x-xlo)/(xhi-xlo)*iw;} function sy(y){return m.t+ih-(y-ylo)/(yhi-ylo)*ih;} var pxPerY=ih/(yhi-ylo);var svg='';svg+='';svg+='';var sse=0;pts.forEach(function(p){var yhat=a+b*p.x,r=p.y-yhat;sse+=r*r;var side=Math.abs(r)*pxPerY,px=sx(p.x),py=sy(p.y),pyh=sy(yhat);var sqx=px,sqy=Math.min(py,pyh);svg+='';svg+='';});svg+='';pts.forEach(function(p){svg+='';});svg+='';chart.innerHTML=svg;var bestSse=0;pts.forEach(function(p){var r=p.y-(best.a+best.b*p.x);bestSse+=r*r;});sseEl.innerHTML='Sum of squared errors '+sse.toFixed(2)+''+' (best possible '+bestSse.toFixed(2)+')';bEl.textContent=b.toFixed(2);aEl.textContent=a.toFixed(2);} bs.addEventListener('input',function(){b=+bs.value;draw();});as.addEventListener('input',function(){a=+as.value;draw();});el.querySelector('.of-snap').addEventListener('click',function(){b=+best.b.toFixed(2);a=+best.a.toFixed(2);bs.value=b;as.value=a;draw();});draw();} function rcode(pts){var rdf=u.rdf(pts,[{name:'x',key:'x'},{name:'y',key:'y'}],'d');return['# The same points. lm() finds the slope + intercept that minimize the','# sum of squared residuals - the squares you were shrinking by hand.',rdf,'','fit <- lm(y ~ x, data = d)','coef(fit) # intercept and slope','sse <- sum(residuals(fit)^2) # the minimum sum of squared errors','sse','','plot(d$x, d$y, pch = 19, xlab = "x", ylab = "y")','abline(fit, col = "forestgreen", lwd = 2)','segments(d$x, d$y, d$x, fitted(fit), col = "darkorange") # the residuals'].join('\n');} window.LessonWidgets.register('ols-fit',mount);})();;(function(){'use strict';var IMP=[['tenure',100],['monthly_spend',74],['total_spend',63],['support_calls',55],['contract_type',43],['num_services',31],['age',19],['payment_method',12]];function asym(m){return 0.115+(m<3?0.020*Math.pow(3-m,1.4):0.003*(m-3)*(m-3));} function jit(t){var x=Math.sin(t*12.9898)*43758.5453;return(x-Math.floor(x))-0.5;} function oobErr(t,m){var a=asym(m);var base=a+(0.33-a)*Math.exp(-t/34);return Math.max(0.045,base+0.012*Math.exp(-t/26)*jit(t));} function mount(el,cfg){var bestMtry=(function(){var b=1,be=asym(1);for(var m=2;m<=8;m++){if(asym(m)
    num.trees100
    '+'
    mtry (of 8 features)3
    '+''+'
    '+'
    '+'
    .
    OOB error
    '+'
    .
    OOB accuracy
    '+'
    .
    Best mtry here
    '+'
    '+'
    Variable importance (impurity)
    '+'
    ';var sT=el.querySelector('.lw-strees'),sM=el.querySelector('.lw-smtry');var tt=el.querySelector('.lw-tt'),tm=el.querySelector('.lw-tm');var cv=el.querySelector('.lw-oob'),ctx=cv.getContext('2d');var errEl=el.querySelector('.lw-err'),accEl=el.querySelector('.lw-acc'),bestEl=el.querySelector('.lw-best'),note=el.querySelector('.lw-note');el.querySelector('.lw-imp').innerHTML=IMP.map(function(f){return'
    '+f[0]+'
    '+f[1]+'
    ';}).join('');function draw(){var T=+sT.value,M=+sM.value;tt.textContent=T;tm.textContent=M;var W=cv.width,H=cv.height;ctx.clearRect(0,0,W,H);var mL=64,mR=22,mT=20,mB=52,pW=W-mL-mR,pH=H-mT-mB,eMin=0.04,eMax=0.34;function X(t){return mL+(t-1)/499*pW;}function Y(e){return mT+(1-(e-eMin)/(eMax-eMin))*pH;} ctx.font="500 17px 'IBM Plex Mono',monospace";ctx.textBaseline="middle";[0.10,0.15,0.20,0.25,0.30].forEach(function(e){var y=Y(e);ctx.strokeStyle="#eef1f6";ctx.lineWidth=1.5;ctx.beginPath();ctx.moveTo(mL,y);ctx.lineTo(W-mR,y);ctx.stroke();ctx.fillStyle="#97a0b2";ctx.textAlign="right";ctx.fillText((e*100)+"%",mL-10,y);});ctx.textAlign="center";ctx.textBaseline="top";[1,100,200,300,400,500].forEach(function(t){ctx.fillStyle="#97a0b2";ctx.fillText(t,X(t),H-mB+12);});ctx.fillStyle="#677084";ctx.font="600 18px 'IBM Plex Sans',sans-serif";ctx.textAlign="center";ctx.fillText("number of trees",mL+pW/2,H-20);ctx.save();ctx.translate(20,mT+pH/2);ctx.rotate(-Math.PI/2);ctx.fillText("OOB error",0,0);ctx.restore();var aY=Y(asym(M));ctx.setLineDash([7,6]);ctx.strokeStyle="#c2410c";ctx.lineWidth=2;ctx.beginPath();ctx.moveTo(mL,aY);ctx.lineTo(W-mR,aY);ctx.stroke();ctx.setLineDash([]);ctx.fillStyle="#c2410c";ctx.font="600 16px 'IBM Plex Mono',monospace";ctx.textAlign="left";ctx.textBaseline="bottom";ctx.fillText("floor "+(asym(M)*100).toFixed(1)+"%",mL+8,aY-4);ctx.strokeStyle="rgba(31,122,85,.22)";ctx.lineWidth=2;ctx.beginPath();for(var t=1;t<=500;t+=2){var x=X(t),y=Y(oobErr(t,M));t===1?ctx.moveTo(x,y):ctx.lineTo(x,y);}ctx.stroke();ctx.strokeStyle="#1f7a55";ctx.lineWidth=3.4;ctx.lineJoin="round";ctx.beginPath();for(var t2=1;t2<=T;t2++){var x2=X(t2),y2=Y(oobErr(t2,M));t2===1?ctx.moveTo(x2,y2):ctx.lineTo(x2,y2);}ctx.stroke();var mx=X(T),my=Y(oobErr(T,M));ctx.beginPath();ctx.arc(mx,my,7,0,7);ctx.fillStyle="#1f7a55";ctx.fill();ctx.lineWidth=3;ctx.strokeStyle="#fff";ctx.stroke();var e=oobErr(T,M);errEl.textContent=(e*100).toFixed(1)+'%';accEl.textContent=((1-e)*100).toFixed(1)+'%';bestEl.textContent='mtry '+bestMtry;var parts=[];if(M<3)parts.push('mtry '+M+' is low: each tree is starved of features, so the floor sits above its best.');else if(M>5)parts.push('mtry '+M+' is high: trees re-correlate (all chasing the strong features) and the floor creeps back up.');else parts.push('mtry '+M+' ≈ √8: the sweet spot, the floor is as low as it goes.');if(T<60)parts.push(' The curve is still falling, drag trees higher.');else if(T>=220)parts.push(' Past ~200 trees the curve is flat, more trees just cost time.');else parts.push(' The error has nearly settled.');note.innerHTML=parts.join('');} sT.addEventListener('input',draw);sM.addEventListener('input',draw);draw();} if(window.LessonWidgets)window.LessonWidgets.register('oob-tuner',mount);})();;(function(){'use strict';var u=window.LessonWidgets.u,P=u.P;var THR={strict:{q:13.82,pct:'99.9%'},medium:{q:9.21,pct:'99%'},loose:{q:5.99,pct:'95%'}};function rng(seed){var s=seed>>>0;return function(){s=(s*1664525+1013904223)>>>0;return s/4294967296;};} function build(){var r=rng(2);var inl=[],ood=[];for(var i=0;i<400;i++){inl.push(-2*Math.log(Math.max(r(),1e-9)));ood.push(-2*Math.log(Math.max(r(),1e-9))+26+12*r());} return{inl:inl,ood:ood};} function mount(el,cfg){cfg=cfg||{};var level='medium';var d=build();el.style.cssText='border:1px solid '+P.line+';border-radius:12px;background:#fff;padding:16px 17px';el.innerHTML='
    '+u.seg([{v:'loose',label:'Loose (95%)'},{v:'medium',label:'Medium (99%)'},{v:'strict',label:'Strict (99.9%)'}],'medium')+'
    '+'
    '+'
    '+ u.runnable(rcode(),{label:'OOD detection: Mahalanobis distance + chi-square threshold, base R'});var plot=el.querySelector('.od-plot'),read=el.querySelector('.od-read');var W=340,H=180,PAD=10,T=10,Bm=22,XMAX=55,NB=44;function px(s){return PAD+Math.min(s,XMAX)/XMAX*(W-2*PAD);} function hist(a){var h=new Array(NB).fill(0);a.forEach(function(s){var k=Math.min(NB-1,Math.floor(s/XMAX*NB));h[k]++;});return h;} function draw(){var thr=THR[level].q;var hi=hist(d.inl),ho=hist(d.ood);var mx=Math.max(Math.max.apply(null,hi),Math.max.apply(null,ho))||1;var bw=(W-2*PAD)/NB;function bars(h,col){return h.map(function(c,k){var hh=c/mx*(H-T-Bm);return'';}).join('');} var fpr=d.inl.filter(function(s){return s>thr;}).length/d.inl.length;var det=d.ood.filter(function(s){return s>thr;}).length/d.ood.length;plot.innerHTML=''+ bars(hi,P.c0)+bars(ho,P.bad)+''+'flag >'+'inliers'+'out-of-distribution'+'novelty score (Mahalanobis distance) ->';read.innerHTML='At the '+THR[level].pct+' cutoff, '+Math.round(fpr*100)+'% of genuine inliers are wrongly flagged (false positives) and '+Math.round(det*100)+'% of true out-of-distribution points are caught. '+ (level==='strict'?'A strict cutoff nearly eliminates false alarms but can miss subtle novelty.':level==='loose'?'A loose cutoff catches everything odd but at a higher false-alarm cost.':'A middle cutoff balances the two, the usual starting point.');} u.wireSeg(el.querySelector('.od-seg'),function(v){level=v;draw();});draw();} function rcode(){return['# Out-of-distribution detection: score novelty by Mahalanobis distance, threshold it. Base R.','set.seed(2)','n <- 500; X <- cbind(rnorm(n), rnorm(n)) # training data: a 2D Gaussian blob','mu <- colMeans(X); S <- cov(X)','score <- function(pts) mahalanobis(pts, mu, S) # squared Mahalanobis distance = OOD score','thr <- qchisq(0.99, df = 2) # cutoff: chi-square 2 df, 99th percentile','','test_in <- cbind(rnorm(200), rnorm(200)) # in-distribution','test_ood <- cbind(rnorm(200, 4), rnorm(200, 4)) # out-of-distribution (shifted 4 sd away)','round(c(threshold = thr,',' flagged_inlier = mean(score(test_in) > thr), # false-positive rate ~ 0.01',' flagged_ood = mean(score(test_ood) > thr)), # detection rate ~ 0.99',' 3)','#> threshold flagged_inlier flagged_ood','#> 9.210 0.015 0.990'].join('\n');} window.LessonWidgets.register('ood-detect',mount);})();;(function(){'use strict';var u=window.LessonWidgets.u,P=u.P;var CATS=['low','medium','high'];var COL=[P.c1,P.faint,P.acc];var BETA=1.1,CUT=[-1.2,1.0];function invlogit(z){return 1/(1+Math.exp(-z));} function probs(x){var eta=BETA*x;var c1=invlogit(CUT[0]-eta),c2=invlogit(CUT[1]-eta);return[c1,c2-c1,1-c2];} function mount(el,cfg){cfg=cfg||{};var xv=0;el.style.cssText='border:1px solid '+P.line+';border-radius:12px;background:#fff;padding:16px 17px';el.innerHTML='
    '+''+'
    '+ u.runnable(rcode(),{label:'A proportional-odds model with MASS::polr()'});var plot=el.querySelector('.oc-plot'),read=el.querySelector('.oc-read'),slider=el.querySelector('.oc-s'),vEl=el.querySelector('.oc-v');var W=440,H=210,m={l:40,r:90,t:12,b:26},iw=W-m.l-m.r,ih=H-m.t-m.b;function draw(){var svg='';var N=60,sx=function(i){return m.l+i/N*iw;};var lowerY=[];for(var i=0;i<=N;i++)lowerY.push(m.t+ih);for(var c=0;c<3;c++){var top=[],bot=[];for(var j=0;j<=N;j++){var x=-3+6*j/N,pr=probs(x);var cum=0;for(var cc=0;cc<=c;cc++)cum+=pr[cc];var y=m.t+ih-cum*ih;top.push(sx(j)+','+y.toFixed(1));} for(var k=N;k>=0;k--)bot.push(sx(k)+','+lowerY[k].toFixed(1));svg+='';for(var jj=0;jj<=N;jj++){var x2=-3+6*jj/N,pr2=probs(x2),cum2=0;for(var c2=0;c2<=c;c2++)cum2+=pr2[c2];lowerY[jj]=m.t+ih-cum2*ih;}} var mi=(xv+3)/6*N;svg+='';CATS.forEach(function(nm,i){svg+=''+nm+'';});svg+='predictor value';svg+='';plot.innerHTML=svg;var pr=probs(xv);vEl.textContent=xv.toFixed(1);read.innerHTML='At this predictor value: P(low) '+(pr[0]*100).toFixed(0)+'%, P(medium) '+(pr[1]*100).toFixed(0)+'%, P(high) '+(pr[2]*100).toFixed(0)+'%. '+'As the predictor rises, probability flows steadily from "low" up to "high". The proportional-odds assumption is that one slope drives every cutpoint, so the bands shift together and never cross.';} slider.addEventListener('input',function(){xv=+slider.value;draw();});draw();} function rcode(){return['# An ordered outcome (low < medium < high) modeled with proportional odds.','library(MASS)','set.seed(1); n <- 600','x <- rnorm(n)','eta <- 1.1 * x','p_le1 <- plogis(-1.2 - eta); p_le2 <- plogis(1.0 - eta) # cumulative probs','u <- runif(n)','y <- factor(ifelse(u < p_le1, "low", ifelse(u < p_le2, "medium", "high")),',' levels = c("low","medium","high"), ordered = TRUE)','','m <- polr(y ~ x, Hess = TRUE)','coef(m) # one slope; positive x shifts mass toward "high"'].join('\n');} window.LessonWidgets.register('ordinal-cumlogit',mount);})();;(function(){'use strict';var u=window.LessonWidgets.u,P=u.P;var GROUPS=[{c:P.c0,pts:[[-2.1,1.0],[-1.6,1.5],[-2.4,0.5],[-1.2,0.9],[-1.9,1.8]]},{c:P.acc,pts:[[0.2,-0.3],[0.7,0.1],[-0.1,0.4],[0.5,-0.6],[0.0,0.0]]},{c:P.c2||'#c9a24a',pts:[[2.0,-1.1],[1.6,-1.6],[2.4,-0.7],[1.3,-1.0],[1.9,-1.7]]}];var VAR=[0.72,0.21,0.07];function mount(el,cfg){cfg=cfg||{};el.style.cssText='border:1px solid '+P.line+';border-radius:12px;background:#fff;padding:16px 17px';el.innerHTML='
    '+'
    '+'
    '+'
    '+'
    PC1 is the single direction along which the data spreads most; PC2 is the best remaining direction at right angles to it. Together the first two components keep '+Math.round((VAR[0]+VAR[1])*100)+'% of the variance, so a 2D picture barely loses anything.
    '+ u.runnable(rcode(),{label:'Run prcomp and plot the projection in R'});var S=240,m=26,iw=S-m-12,ih=S-m-12;var xs=[],ys=[];GROUPS.forEach(function(g){g.pts.forEach(function(p){xs.push(p[0]);ys.push(p[1]);});});var x0=Math.min.apply(null,xs),x1=Math.max.apply(null,xs),y0=Math.min.apply(null,ys),y1=Math.max.apply(null,ys);function px(x){return m+(x-x0)/(x1-x0)*iw;}function py(y){return(S-m)-(y-y0)/(y1-y0)*ih;} var svg='';svg+='';GROUPS.forEach(function(g){g.pts.forEach(function(p){svg+='';});});svg+='PC1 ('+Math.round(VAR[0]*100)+'%)';svg+='PC2 ('+Math.round(VAR[1]*100)+'%)';svg+='';el.querySelector('.pp-sc').innerHTML=svg;var vb='
    variance explained
    ';VAR.forEach(function(v,i){vb+='
    PC'+(i+1)+''+''+''+Math.round(v*100)+'%
    ';});el.querySelector('.pp-var').innerHTML=vb;} function rcode(){return['# PCA finds new axes (principal components) ordered by how much variance they capture.','p <- prcomp(iris[, 1:4], scale. = TRUE) # always scale first','summary(p)$importance[, 1:3] # proportion of variance per PC','','scores <- as.data.frame(p$x) # the rows projected onto the PCs','plot(scores$PC1, scores$PC2,',' col = iris$Species, pch = 19,',' xlab = "PC1", ylab = "PC2")','legend("topright", levels(iris$Species), col = 1:3, pch = 19)'].join('\n');} window.LessonWidgets.register('pca-projection',mount);})();;(function(){'use strict';var u=window.LessonWidgets.u,P=u.P;var GRID=[];for(var g=0;g<=20;g++)GRID.push(g/20);var ROWS=[{off:0.00,k:6},{off:0.10,k:5},{off:-0.08,k:7},{off:0.18,k:9},{off:-0.14,k:4},{off:0.04,k:6}];function f(x,r){return 0.5+r.off+0.42*Math.tanh(r.k*(x-0.5));} function mount(el,cfg){cfg=cfg||{};el.style.cssText='border:1px solid '+P.line+';border-radius:12px;background:#fff;padding:16px 17px';el.innerHTML='
    '+'
    Each faint line is one row\'s prediction as the feature sweeps left to right (an ICE curve). The bold line is their average - the partial-dependence curve. When the faint lines fan out instead of running parallel, the feature interacts with others and the average alone would mislead.
    '+ u.runnable(rcode(),{label:'Build a partial-dependence curve in R'});var S=260,W=420,m=34,iw=W-m-14,ih=S-m-14;function px(x){return m+x*iw;}function py(y){return(S-m)-y*ih;} var svg='';svg+='';ROWS.forEach(function(r){var pts=GRID.map(function(x){return px(x).toFixed(1)+','+py(f(x,r)).toFixed(1);}).join(' ');svg+='';});var avg=GRID.map(function(x){var s=0;ROWS.forEach(function(r){s+=f(x,r);});return px(x).toFixed(1)+','+py(s/ROWS.length).toFixed(1);}).join(' ');svg+='';svg+='feature value';svg+='predicted outcome';svg+='';el.querySelector('.pd-plot').innerHTML=svg;} function rcode(){return['# Partial dependence: sweep ONE feature across a grid, holding the rest at their','# real values, predict for every row, and average. The average is the PDP.','set.seed(1)','n <- 300','d <- data.frame(x1 = runif(n), x2 = runif(n))','d$y <- 1 / (1 + exp(-6 * (d$x1 - 0.5))) + 0.4 * d$x2 + rnorm(n, 0, 0.1)','fit <- lm(y ~ poly(x1, 3) + x2, data = d)','','grid <- seq(0, 1, length.out = 25)','pdp <- sapply(grid, function(v) {',' tmp <- d; tmp$x1 <- v # force x1 = v for every row',' mean(predict(fit, tmp)) # average the predictions','})','plot(grid, pdp, type = "l", lwd = 2, xlab = "x1", ylab = "avg prediction")'].join('\n');} window.LessonWidgets.register('pdp-curve',mount);})();;(function(){'use strict';var u=window.LessonWidgets.u,P=u.P;var ZA=1.959964;var EFFECTS={small:0.2,medium:0.5,large:0.8};function erf(x){var t=1/(1+0.3275911*Math.abs(x));var y=1-(((((1.061405429*t-1.453152027)*t)+1.421413741)*t-0.284496736)*t+0.254829592)*t*Math.exp(-x*x);return x>=0?y:-y;} function pnorm(x){return 0.5*(1+erf(x/Math.SQRT2));} function power(d,n){var ncp=d*Math.sqrt(n/2);return pnorm(ncp-ZA)+pnorm(-ncp-ZA);} function nFor80(d){for(var n=2;n<4000;n++){if(power(d,n)>=0.8)return n;}return 4000;} function mount(el,cfg){cfg=cfg||{};var eff='medium';el.style.cssText='border:1px solid '+P.line+';border-radius:12px;background:#fff;padding:16px 17px';el.innerHTML='
    '+u.seg([{v:'small',label:'Small effect (d=0.2)'},{v:'medium',label:'Medium (d=0.5)'},{v:'large',label:'Large (d=0.8)'}],'medium')+'
    '+'
    '+'
    '+ u.runnable(rcode(),{label:'Sample size for 80% power, base R power.t.test'});var plot=el.querySelector('.pw-plot'),read=el.querySelector('.pw-read');var W=340,H=190,NMAX=300,PAD=30;function px(n){return PAD+(n/NMAX)*(W-PAD-8);} function py(p){return(H-22)-p*(H-34);} function draw(){var d=EFFECTS[eff];var pts=[];for(var n=2;n<=NMAX;n+=2)pts.push(px(n).toFixed(1)+','+py(power(d,n)).toFixed(1));var n80=nFor80(d),x80=px(Math.min(n80,NMAX)),y80=py(0.8);var grid='';[0.25,0.5,0.8].forEach(function(p){grid+=''+p+'';});plot.innerHTML=''+ grid+''+ (n80<=NMAX?'':'')+'power'+'sample size n per group';read.innerHTML='A '+eff+' effect (d = '+d+') needs n ≈ '+n80+' per group to reach 80% power at α = 0.05. '+ (eff==='small'?'Small effects are expensive: detecting them reliably takes hundreds per arm.':eff==='large'?'Large effects are cheap to detect: a couple of dozen per arm suffices.':'Halve the effect and the required sample roughly quadruples: power scales with n times d squared.');} u.wireSeg(el.querySelector('.pw-seg'),function(v){eff=v;draw();});draw();} function rcode(){return['# How many subjects per group for 80% power? Base R, no packages.','# Two-sample t-test, alpha = 0.05 two-sided, effect measured in standard deviations (sd = 1).','sizes <- sapply(c(small = 0.2, medium = 0.5, large = 0.8), function(d)',' ceiling(power.t.test(delta = d, sd = 1, sig.level = 0.05, power = 0.80)$n))','sizes','','# and the reverse: the power you actually get from n = 64 per group','round(sapply(c(small = 0.2, medium = 0.5, large = 0.8), function(d)',' power.t.test(n = 64, delta = d, sd = 1, sig.level = 0.05)$power), 3)','# small effects need hundreds per arm; large effects only dozens.'].join('\n');} window.LessonWidgets.register('power-curve',mount);})();;(function(){'use strict';var u=window.LessonWidgets.u,P=u.P;var T_OBS=15;var MU=1.6,SD=1.4;function rng(seed){var s=seed>>>0;return function(){s=(s*1664525+1013904223)>>>0;return s/4294967296;};} function reps(model,nrep){var r=rng(model==='poisson'?11:23),spare=null;function rnorm(){if(spare!==null){var v=spare;spare=null;return v;}var a=Math.max(r(),1e-12),b=r(),m=Math.sqrt(-2*Math.log(a));spare=m*Math.sin(2*Math.PI*b);return m*Math.cos(2*Math.PI*b);} function rpois(l){var L=Math.exp(-l),k=0,p=1;do{k++;p*=r();}while(p>L);return k-1;} var out=[];for(var j=0;j'+'
    '+'
    '+ u.runnable(rcode(),{label:'A posterior predictive check with the number-of-zeros statistic, base R'});var plot=el.querySelector('.pp-plot'),read=el.querySelector('.pp-read');var W=340,H=170,XMAX=24;function bx(t){return(t/XMAX)*(W-8)+4;} function draw(){var rp=reps(model,2000);var bins=new Array(XMAX+1).fill(0);rp.forEach(function(t){if(t>=0&&t<=XMAX)bins[t]++;});var mx=Math.max.apply(null,bins)||1,bw=(W-8)/(XMAX+1);var ppc=rp.filter(function(t){return t>=T_OBS;}).length/rp.length;var bars=bins.map(function(c,t){var h=c/mx*(H-34);return'';}).join('');var obsX=bx(T_OBS).toFixed(1);plot.innerHTML=''+ bars+''+'observed = '+T_OBS+''+'number of zeros in a replicated dataset';read.innerHTML=(model==='poisson'?'The Poisson replicates cluster around 11-12 zeros, and the observed 15 sits comfortably inside the crowd. ':'The Normal replicates almost never reach 15 zeros (they average ~8), so the observed 15 lands far out in the tail. ')+'PPC p-value ≈ '+ppc.toFixed(2)+': '+(ppc>0.05&&ppc<0.95?'the model reproduces this feature of the data.':'the model fails to reproduce the zeros, a clear misfit.');} u.wireSeg(el.querySelector('.pp-seg'),function(v){model=v;draw();});draw();} function rcode(){return['# Posterior predictive check: does the model reproduce the number of zeros? Base R.','set.seed(2)','y <- rpois(60, 1.5) # observed zero-heavy counts','T_obs <- sum(y == 0) # test statistic: number of zeros','mu <- mean(y); s <- sd(y)','','# replicate the statistic under each fitted model','T_normal <- replicate(3000, sum(round(rnorm(60, mu, s)) == 0)) # wrong: a Normal','T_pois <- replicate(3000, sum(rpois(60, mu) == 0)) # right: a Poisson','','round(c(T_obs = T_obs,',' ppc_p_normal = mean(T_normal >= T_obs), # near 0 => the Normal misfits',' ppc_p_pois = mean(T_pois >= T_obs)), # mid-range => the Poisson is fine',' 3)','# the Normal predicts far too few zeros (tiny PPC p); the Poisson reproduces them.'].join('\n');} window.LessonWidgets.register('ppc-overlay',mount);})();;(function(){'use strict';var INK='#131720',MUT='#677084',ACC='#1f7a55',BG='#f3f6f4',LINE='#c5cdda';var _uid=0;function mount(el,cfg){cfg=cfg||{};var steps=(cfg.steps&&cfg.steps.length)?cfg.steps:[{title:'Bootstrap',sub:'grow each tree on its own random resample of the rows'},{title:'Random features',sub:'at each split, consider only a random subset (mtry)'},{title:'Average',sub:'all trees vote; the majority or mean is the forest answer'}];var n=steps.length,boxH=62,gap=24,padY=6;var H=padY*2+n*boxH+(n-1)*gap,mid='pf-ar'+(++_uid),out='',i;for(i=0;i'+''+''+(i+1)+''+''+steps[i].title+''+''+steps[i].sub+'';if(i';} var svg=''+''+out+'';el.innerHTML='
    '+svg+'
    ';} if(window.LessonWidgets)window.LessonWidgets.register('process-flow',mount);})();;(function(){'use strict';var u=window.LessonWidgets.u,P=u.P;var D=(function(){var a=[],s=5;function r(){s=(s*1103515245+12345)&0x7fffffff;return s/0x7fffffff-0.5;} for(var i=0;i<90;i++){var x=1+(i/89)*18;a.push({x:x,y:20+2.2*x+r()*(6+2.4*x)});} return a;})();var Q={'0.1':[16,1.0],'0.5':[20,2.2],'0.9':[24,3.4]};function mount(el,cfg){cfg=cfg||{};var tau='0.5';el.style.cssText='border:1px solid '+P.line+';border-radius:12px;background:#fff;padding:16px 17px';el.innerHTML='
    '+u.seg([{v:'0.1',label:'10th %ile'},{v:'0.5',label:'median'},{v:'0.9',label:'90th %ile'}],'0.5')+'
    '+'
    '+'
    '+ u.runnable(rcode(),{label:'A quantile line by minimizing the pinball loss (base R)'});var plot=el.querySelector('.ql-plot'),read=el.querySelector('.ql-read');var W=440,H=250,m={l:42,r:12,t:12,b:30},iw=W-m.l-m.r,ih=H-m.t-m.b;var xe=[0,20],ye=[0,120];function sx(x){return m.l+(x-xe[0])/(xe[1]-xe[0])*iw;} function sy(y){return m.t+ih-(y-ye[0])/(ye[1]-ye[0])*ih;} function draw(){var svg='';svg+='';D.forEach(function(d){svg+='';});['0.1','0.5','0.9'].forEach(function(q){var b=Q[q],on=q===tau,col=on?P.acc:P.faint;svg+='';svg+='τ='+q+'';});svg+='years of experience';svg+='income';svg+='';plot.innerHTML=svg;var b=Q[tau];read.innerHTML=tau==='0.5'?'The median line (slope '+b[1].toFixed(1)+') is close to what OLS would give. But it is only the middle of the story.':'The τ='+tau+' line has slope '+b[1].toFixed(1)+'. The 90th-percentile line rises far steeper than the 10th: each extra year of experience buys the top earners much more than the bottom. The lines fan apart because the spread grows with x, which a single OLS line can never show.';} u.wireSeg(el.querySelector('.ql-seg'),function(v){tau=v;draw();});draw();} function rcode(){return['# Quantile regression fits a line to a percentile by minimizing the pinball loss.','set.seed(5); n <- 300','x <- runif(n, 1, 19)','y <- 20 + 2.2 * x + rnorm(n) * (6 + 2.4 * x) # spread grows with x (heteroskedastic)','','pinball <- function(b, tau) { r <- y - (b[1] + b[2]*x); sum(r * (tau - (r < 0))) }','fit_q <- function(tau) optim(c(20, 2), pinball, tau = tau)$par','rbind(q10 = fit_q(0.1), median = fit_q(0.5), q90 = fit_q(0.9))','# the slope rises across quantiles: the top earners gain more per year than the bottom.'].join('\n');} window.LessonWidgets.register('quantile-lines',mount);})();;(function(){'use strict';var u=window.LessonWidgets.u,P=u.P;function rng(seed){var s=seed>>>0;return function(){s=(s*1664525+1013904223)>>>0;return s/4294967296;};} var BW={narrow:0.3,medium:0.6,wide:1.0};function build(){var r=rng(2),spare=null;function rnorm(){if(spare!==null){var v=spare;spare=null;return v;}var a=Math.max(r(),1e-12),b=r(),m=Math.sqrt(-2*Math.log(a));spare=m*Math.sin(2*Math.PI*b);return m*Math.cos(2*Math.PI*b);} var pts=[];for(var i=0;i<500;i++){var x=r()*2-1,t=x>=0?1:0;pts.push({x:x,y:3+2*x+4*t+rnorm(),t:t});} return pts;} function ols(pts){var n=pts.length;if(n<2)return{a:0,b:0};var mx=0,my=0;pts.forEach(function(p){mx+=p.x;my+=p.y;});mx/=n;my/=n;var sxy=0,sxx=0;pts.forEach(function(p){sxy+=(p.x-mx)*(p.y-my);sxx+=(p.x-mx)*(p.x-mx);});var b=sxx?sxy/sxx:0;return{a:my-b*mx,b:b};} function mount(el,cfg){cfg=cfg||{};var band='medium';var pts=build();el.style.cssText='border:1px solid '+P.line+';border-radius:12px;background:#fff;padding:16px 17px';el.innerHTML='
    '+u.seg([{v:'narrow',label:'Narrow band (0.3)'},{v:'medium',label:'Medium (0.6)'},{v:'wide',label:'Wide (1.0)'}],'medium')+'
    '+'
    '+'
    '+ u.runnable(rcode(),{label:'Sharp RDD: a local line on each side of the cutoff, base R'});var plot=el.querySelector('.rd-plot'),read=el.querySelector('.rd-read');var W=340,H=200,PADL=8,PADR=8,T=10,Bm=22;function px(x){return PADL+(x+1)/2*(W-PADL-PADR);} function py(y){return T+(11-y)/(11-(-1))*(H-T-Bm);} function draw(){var h=BW[band];var L=pts.filter(function(p){return p.x<0&&p.x>=-h;});var Rr=pts.filter(function(p){return p.x>=0&&p.x<=h;});var fl=ols(L),fr=ols(Rr),jump=fr.a-fl.a;var dots=pts.map(function(p){var inb=Math.abs(p.x)<=h;return'';}).join('');var x0=px(0);var lseg='';var rseg='';var jseg='';plot.innerHTML=''+''+ dots+lseg+rseg+jseg+'jump '+jump.toFixed(2)+''+'below cutoff'+'above (treated)';read.innerHTML='Fitting a line just below and just above the cutoff, the outcome jumps by '+jump.toFixed(2)+' at the threshold (true effect 4). '+ (band==='narrow'?'A narrow band uses only points near the cutoff, so it is less biased but noisier.':band==='wide'?'A wide band borrows far-away points, steadier but biased if the true relationship curves.':'The bandwidth trades bias (wide) against variance (narrow); this is the central RDD tuning choice.');} u.wireSeg(el.querySelector('.rd-seg'),function(v){band=v;draw();});draw();} function rcode(){return['# Sharp regression discontinuity: the outcome jumps at a cutoff. Base R.','set.seed(2)','n <- 500; x <- runif(n, -1, 1) # running variable, cutoff at 0','treat <- as.integer(x >= 0) # sharp: treated iff x >= cutoff','y <- 3 + 2 * x + 4 * treat + rnorm(n) # the TRUE jump at the cutoff is 4','','h <- 0.5 # bandwidth: keep points within h of the cutoff','left <- lm(y ~ x, subset = x < 0 & x >= -h)','right <- lm(y ~ x, subset = x >= 0 & x <= h)','jump <- predict(right, data.frame(x = 0)) - predict(left, data.frame(x = 0))','round(c(true = 4, rdd = unname(jump),',' naive = mean(y[treat==1]) - mean(y[treat==0])), 2)','#> true rdd naive','#> 4.00 3.79 6.09 # naive is biased upward by the slope in x'].join('\n');} window.LessonWidgets.register('rdd-cutoff',mount);})();;(function(){'use strict';function mount(el,cfg){var u=window.LessonWidgets.u;if(!u)return;cfg=cfg||{};var text=cfg.text||'Order A12 shipped on 2026-03-08 to dev@site.org for $45.90';var pats=cfg.patterns||[{src:'\\d+',label:'Digits'},{src:'\\d{4}-\\d{2}-\\d{2}',label:'Date'},{src:'[\\w.]+@[\\w.]+',label:'Email'},{src:'\\$\\d+\\.\\d+',label:'Price'}];var cur=0;var wrap=document.createElement('div');wrap.style.cssText='font-family:IBM Plex Sans,system-ui,sans-serif';function highlight(src){var re;try{re=new RegExp(src,'g');}catch(e){return{html:u.esc(text),n:0};} var out='',last=0,m,n=0,guard=0;while((m=re.exec(text))!==null&&guard++<2000){var mt=m[0];if(mt===''){re.lastIndex++;continue;} out+=u.esc(text.slice(last,m.index))+''+u.esc(mt)+'';last=m.index+mt.length;n++;} out+=u.esc(text.slice(last));return{html:out,n:n};} function rLit(src){return src.replace(/\\/g,'\\\\');} function render(){var p=pats[cur],h=highlight(p.src);wrap.querySelector('.rx-str').innerHTML=h.html;wrap.querySelector('.rx-meta').innerHTML='Pattern '+u.esc(p.src)+' matched '+h.n+' time'+(h.n===1?'':'s')+'.';wrap.querySelector('.rx-run').innerHTML=u.runnable('x <- "'+text.replace(/"/g,'\\"')+'"\n'+'regmatches(x, gregexpr("'+rLit(p.src)+'", x))',{label:'Run it in R'});} wrap.innerHTML='
    '+'
    Try a pattern:
    '+'
    '+u.seg(pats.map(function(p,i){return{v:i,label:p.label};}),0)+'
    '+'
    '+'
    '+'
    '+'
    ';u.wireSeg(wrap.querySelector('.rx-seg'),function(v){cur=+v;render();});el.innerHTML='';el.appendChild(wrap);render();} if(window.LessonWidgets)window.LessonWidgets.register('regex-highlight',mount);})();;(function(){'use strict';var u=window.LessonWidgets.u,P=u.P;function rng(seed){var s=seed>>>0;return function(){s=(s*1103515245+12345)&0x7fffffff;return s/0x7fffffff;};} function gauss(r){return(r()+r()+r()+r()-2)*0.8660254;} function mount(el,cfg){cfg=cfg||{};var n=20,A=2,B=0.6,SD=1.3,xlo=0,xhi=10;function data(N){var r=rng(7),d=[];for(var i=0;isample size n = '+''+'
    '+ u.runnable(rcode(),{label:'Confidence vs prediction intervals in R'});var chart=el.querySelector('.ri-chart'),read=el.querySelector('.ri-read'),slider=el.querySelector('.ri-s'),nEl=el.querySelector('.ri-n');function draw(){var d=data(n),N=d.length;var sx=0,sy=0,sxx=0,sxy=0;d.forEach(function(p){sx+=p.x;sy+=p.y;sxx+=p.x*p.x;sxy+=p.x*p.y;});var xbar=sx/N,b=(N*sxy-sx*sy)/(N*sxx-sx*sx),a=(sy-b*sx)/N;var sse=0,Sxx=0;d.forEach(function(p){var e=p.y-(a+b*p.x);sse+=e*e;Sxx+=(p.x-xbar)*(p.x-xbar);});var s=Math.sqrt(sse/Math.max(1,N-2)),tval=2.04;function ci(x){return tval*s*Math.sqrt(1/N+(x-xbar)*(x-xbar)/Sxx);} function pi(x){return tval*s*Math.sqrt(1+1/N+(x-xbar)*(x-xbar)/Sxx);} var W=480,H=280,m={t:14,r:14,b:34,l:40},iw=W-m.l-m.r,ih=H-m.t-m.b;var ylo=A-4,yhi=A+B*10+4;function px(x){return m.l+(x-xlo)/(xhi-xlo)*iw;}function py(y){return m.t+ih-(y-ylo)/(yhi-ylo)*ih;} function band(fn,fill){var up='',dn='';for(var x=xlo;x<=xhi+0.001;x+=0.5){up+=px(x).toFixed(1)+','+py(a+b*x+fn(x)).toFixed(1)+' ';dn=px(x).toFixed(1)+','+py(a+b*x-fn(x)).toFixed(1)+' '+dn;}return'';} var svg='';svg+=band(pi,'rgba(181,99,26,0.13)')+band(ci,'rgba(31,122,85,0.22)');svg+='';d.forEach(function(p){svg+='';});svg+='confidence (mean)prediction (new point)';chart.innerHTML=svg;read.innerHTML='At n='+n+', confidence half-width at the center is ±'+ci(xbar).toFixed(2)+' and prediction is ±'+pi(xbar).toFixed(2)+'. Push n up: the green confidence band collapses onto the line (we pin down the mean), but the orange prediction band barely moves - a single new point still carries the irreducible noise.';nEl.textContent=n;} slider.addEventListener('input',function(){n=+slider.value;draw();});draw();} function rcode(){return['# Two very different intervals. Confidence = uncertainty about the mean line;','# prediction = where a NEW observation could fall (much wider).','set.seed(1)','x <- runif(40, 0, 10); y <- 2 + 0.6 * x + rnorm(40, 0, 1.3)','fit <- lm(y ~ x)','grid <- data.frame(x = seq(0, 10, 0.5))','','ci <- predict(fit, grid, interval = "confidence") # for the average y at x','pi <- predict(fit, grid, interval = "prediction") # for a single new y at x','','plot(x, y, pch = 19, col = "gray60")','abline(fit, col = "forestgreen", lwd = 2)','lines(grid$x, ci[, "lwr"], col = "forestgreen", lty = 2)','lines(grid$x, ci[, "upr"], col = "forestgreen", lty = 2)','lines(grid$x, pi[, "lwr"], col = "darkorange", lty = 3)','lines(grid$x, pi[, "upr"], col = "darkorange", lty = 3)'].join('\n');} window.LessonWidgets.register('regression-intervals',mount);})();;(function(){'use strict';var u=window.LessonWidgets.u;var LIGHT={ink:'#131720',body:'#434b59',mut:'#677084',faint:'#97a0b2',line:'#d8dee9',grid:'#eef1f6',acc:'#1f7a55',c0:'#2563a8',c1:'#b5631a',bad:'#c2410c',panel:'#ffffff',soft:'#f6f8fa',pick:'#e7f3ec'};var DARK={ink:'#eef4fb',body:'#c3d1e3',mut:'#93a4bb',faint:'#6f8299',line:'rgba(255,255,255,.24)',grid:'rgba(255,255,255,.10)',acc:'#46c08a',c0:'#7fb2ea',c1:'#e3a05a',bad:'#f4805a',panel:'#101c2b',soft:'rgba(255,255,255,.05)',pick:'rgba(70,192,138,.16)'};function lumOf(c){c=String(c).trim();var r,g,b,m;if(c.charAt(0)==='#'){if(c.length===4){r=parseInt(c[1]+c[1],16);g=parseInt(c[2]+c[2],16);b=parseInt(c[3]+c[3],16);} else{r=parseInt(c.substr(1,2),16);g=parseInt(c.substr(3,2),16);b=parseInt(c.substr(5,2),16);}}else if((m=c.match(/rgba?\(([^)]+)\)/))){var p=m[1].split(',');r=+p[0];g=+p[1];b=+p[2];} else return 1;return(0.2126*r+0.7152*g+0.0722*b)/255;} function isDark(el){try{var v=getComputedStyle(el).getPropertyValue('--lm-panel');if(v&&v.trim())return lumOf(v)<0.45;}catch(e){} return!!(document.documentElement&&document.documentElement.classList.contains('dark'));} function palette(el){return isDark(el)?DARK:LIGHT;} function lgamma(x){var c=[76.18009172947146,-86.50532032941677,24.01409824083091,-1.231739572450155,0.1208650973866179e-2,-0.5395239384953e-5];var y=x,t=x+5.5,s=1.000000000190015;t-=(x+0.5)*Math.log(t);for(var j=0;j<6;j++)s+=c[j]/++y;return-t+Math.log(2.5066282746310005*s/x);} function betacf(a,b,x){var FPMIN=1e-300,qab=a+b,qap=a+1,qam=a-1,c=1,d=1-qab*x/qap;if(Math.abs(d)=1)return 1;var bt=Math.exp(lgamma(a+b)-lgamma(a)-lgamma(b)+a*Math.log(x)+b*Math.log(1-x));return(x<(a+1)/(a+b+2))?bt*betacf(a,b,x)/a:1-bt*betacf(b,a,1-x)/b;} function pt2(t,df){return ibeta(df/2,0.5,df/(df+t*t));} function qt(p,df){var lo=0,hi=200,i;for(i=0;i<160;i++){var mid=(lo+hi)/2;if(1-pt2(mid,df)/2'+'
    '+'
    '+'
    '+'
    ';var q=el.querySelector('.rf-q'),sub=el.querySelector('.rf-sub'),cards=el.querySelector('.rf-cards'),reveal=el.querySelector('.rf-reveal'),rbox=el.querySelector('.rf-r');cards.addEventListener('click',function(ev){var b=ev.target.closest?ev.target.closest('[data-k]'):null;if(!b||!cards.contains(b))return;picked=b.getAttribute('data-k');revealed=true;render();});function render(){P=palette(el);el.style.background=P.panel;el.style.borderColor=P.line;var s=studies[cur],a=analyse(s,alpha),rs=renders(s,a);q.textContent=revealed?'The same analysis, written four ways':'Which of these reads as the strongest evidence?';q.style.color=P.ink;sub.textContent=revealed?s.label+': '+s.n1+' and '+s.n2+' participants, outcome measured in '+s.unit+'.':'Four sentences from four different papers reporting a trial of the same drug. Pick one.';sub.style.color=P.faint;cards.innerHTML=rs.map(function(r){var on=revealed&&picked===r.key;return'';}).join('');reveal.innerHTML=revealed?revealPanel(studies,cur,alpha,picked,P):'';rbox.innerHTML=revealed?u.runnable(rcode(studies,alpha),{label:'Reproduce every number in R'}):'';var segEl=reveal.querySelector('.rf-seg');if(segEl)u.wireSeg(segEl,function(v){cur=+v;render();});} var wasDark=isDark(el);if(window.MutationObserver){var mo=new MutationObserver(function(){if(!document.contains(el)){mo.disconnect();return;} var now=isDark(el);if(now!==wasDark){wasDark=now;render();}});mo.observe(document.documentElement,{attributes:true,attributeFilter:['class']});} render();} var CHECKS=['How big is it?','How precise is it?','Does it matter?'];function revealPanel(studies,cur,alpha,picked,P){var s=studies[cur],a=analyse(s,alpha),rs=renders(s,a);var h='
    ';h+='
    '+'All four sentences describe the same analysis of the same data. The difference is '+''+a.diff.toFixed(2)+' '+s.unit+', the interval runs '+a.lo.toFixed(2)+' to '+a.hi.toFixed(2)+', and '+fmtPx(a.p)+'. Not one number moved between the cards. What moved is how much of the result you were allowed to see.'+'
    ';h+=''+''+ CHECKS.map(function(c){return'';}).join('')+'';rs.forEach(function(r){h+=''+ r.shows.map(function(v){return'';}).join('')+'';});h+='
    Rendering'+u.esc(c)+'
    '+ u.esc(r.name)+(r.key===picked?' (your pick)':'')+''+(v?'yes':'no')+'
    ';if(studies.length>1){h+='
    Now a second trial with the same p-value
    '+'
    '+ u.seg(studies.map(function(st,i){return{v:i,label:st.label};}),cur)+'
    '+ compare(studies,alpha,P);} return h+'
    ';} function compare(studies,alpha,P){var as=studies.map(function(s){return analyse(s,alpha);});var rows=[['Reported as a bare p-value',as.map(function(a){return fmtP(a.p);})],['Difference',as.map(function(a,i){return a.diff.toFixed(2)+' '+studies[i].unit;})],['95% interval',as.map(function(a){return a.lo.toFixed(2)+' to '+a.hi.toFixed(2);})],['Cohen\'s d',as.map(function(a){return a.d.toFixed(2);})],['Group sizes',as.map(function(a){return a.n1+' and '+a.n2;})]];var h=''+''+ studies.map(function(s){return'';}).join('')+'';rows.forEach(function(r,ri){var same=r[1].every(function(v){return v===r[1][0];});h+=''+ r[1].map(function(v){return'';}).join('')+'';});h+='
    '+u.esc(s.label)+'
    '+u.esc(r[0])+ (ri===0&&same?' identical':'')+''+u.esc(v)+'
    ';var a0=as[0],a1=as[1],m=studies[0].mcid;h+='
    '+'Rounded to two places both trials report the same p-value, so a reader given only that number cannot tell them apart. '+'They are not remotely alike. '+studies[0].label+' is precise and small: the whole interval sits '+ (m&&a0.hi';return h;} function rcode(studies,alpha){var L=['# Both trials, rebuilt with exactly the means and spreads on screen.','set.seed(1)',''];L.push('make <- function(n, m, s) m + s * as.numeric(scale(rnorm(n)))');L.push('');L.push('report <- function(x, y, label) {');L.push(' tt <- t.test(x, y, var.equal = TRUE)');L.push(' sp <- sqrt(((length(x) - 1) * var(x) + (length(y) - 1) * var(y)) /');L.push(' (length(x) + length(y) - 2))');L.push(' cat(label, ": diff", round(unname(diff(rev(tt$estimate))), 2),');L.push(' " CI", round(tt$conf.int[1], 2), "to", round(tt$conf.int[2], 2),');L.push(' " t", round(unname(tt$statistic), 2), " p", round(tt$p.value, 3),');L.push(' " d", round(unname(diff(rev(tt$estimate))) / sp, 2), "\\n")');L.push('}');L.push('');studies.forEach(function(s,i){L.push('a'+i+' <- make('+s.n1+', '+s.m1+', '+s.sd+') # control');L.push('b'+i+' <- make('+s.n2+', '+s.m2+', '+s.sd+') # treated');L.push('report(b'+i+', a'+i+', "'+s.label+'")');});return L.join('\n');} window.LessonWidgets.register('report-four-ways',mount);})();;(function(){'use strict';var u=window.LessonWidgets.u;var LIGHT={ink:'#131720',body:'#434b59',mut:'#677084',faint:'#97a0b2',line:'#d8dee9',grid:'#eef1f6',acc:'#1f7a55',c0:'#2563a8',c1:'#b5631a',bad:'#c2410c',panel:'#ffffff',soft:'#f6f8fa',add:'#e7f3ec',del:'#fbeae5',codeBg:'#0d1117',codeFg:'#e6edf3'};var DARK={ink:'#eef4fb',body:'#c3d1e3',mut:'#93a4bb',faint:'#6f8299',line:'rgba(255,255,255,.24)',grid:'rgba(255,255,255,.10)',acc:'#46c08a',c0:'#7fb2ea',c1:'#e3a05a',bad:'#f4805a',panel:'#101c2b',soft:'rgba(255,255,255,.05)',add:'rgba(70,192,138,.18)',del:'rgba(244,128,90,.18)',codeBg:'#0a0f16',codeFg:'#e6edf3'};function lumOf(c){c=String(c).trim();var r,g,b,m;if(c.charAt(0)==='#'){if(c.length===4){r=parseInt(c[1]+c[1],16);g=parseInt(c[2]+c[2],16);b=parseInt(c[3]+c[3],16);} else{r=parseInt(c.substr(1,2),16);g=parseInt(c.substr(3,2),16);b=parseInt(c.substr(5,2),16);}}else if((m=c.match(/rgba?\(([^)]+)\)/))){var p=m[1].split(',');r=+p[0];g=+p[1];b=+p[2];} else return 1;return(0.2126*r+0.7152*g+0.0722*b)/255;} function isDark(el){try{var v=getComputedStyle(el).getPropertyValue('--lm-panel');if(v&&v.trim())return lumOf(v)<0.45;}catch(e){} return!!(document.documentElement&&document.documentElement.classList.contains('dark'));} function palette(el){return isDark(el)?DARK:LIGHT;} function script(seed){return[{t:'# analysis.R - effect of the programme on follow-up score'},{t:'library(dplyr)'},{fault:'order',broken:'',fixed:'library(MASS) # every package attached at the top, where you can see them'},{t:''},{fault:'path',broken:'raw <- read.csv("C:/Users/sam/Desktop/final_v3 (2).csv")',fixed:'raw <- read.csv("data/trial.csv") # a path inside the project folder'},{fault:'state',broken:'trial <- cleaned %>% filter(!is.na(score)) # where does `cleaned` come from?',fixed:'trial <- raw %>% filter(!is.na(score))'},{t:''},{fault:'order',broken:'library(MASS) # attached halfway down, after dplyr',fixed:''},{fault:'order',broken:'keep <- select(trial, id, arm, score) # whose select() is this now?',fixed:'keep <- dplyr::select(trial, id, arm, score)'},{t:''},{fault:'seed',broken:'',fixed:'set.seed('+seed+') # the bootstrap is now repeatable'},{t:'boot <- replicate(2000, {'},{t:' i <- sample(nrow(keep), replace = TRUE)'},{t:' diff(tapply(keep$score[i], keep$arm[i], mean))'},{t:'})'},{t:''},{t:'cat("effect", round(mean(boot), 3),'},{t:' " 95% CI", round(quantile(boot, c(.025, .975)), 3), "\\n")'}];} var FAULTS={state:{name:'Hidden state',why:'The script asks for an object called `cleaned`. Nothing in the script creates it. It exists on the author\'s machine because it was typed into the console weeks ago and never written down.',err:'Error in filter(., !is.na(score)) : object \'cleaned\' not found',fix:'Build every object the script needs inside the script.'},path:{name:'Absolute path',why:'That path names one folder on one laptop. A co-author, a reviewer, a server and your own next machine all fail on the same line.',err:'Error in file(file, "rt") : cannot open file \'C:/Users/sam/Desktop/final_v3 (2).csv\': No such file or directory',fix:'Use a path relative to the project folder, and ship the folder.'},seed:{name:'No seed',why:'The bootstrap draws random samples. With no seed, R starts from a different place every session, so the interval you publish is not the interval anyone else gets.',err:'Ran without error, and produced a DIFFERENT answer than the run before it.',fix:'Set a seed before anything random, and state it in the paper.'},order:{name:'Load order',why:'MASS and dplyr both export a function called `select`, and they are not the same function. Whichever package was attached last wins. Here MASS is attached halfway down the file, so the same `select()` call means one thing above that line and another below it, and it also depends on what was already attached in the session.',err:'Error in select(trial, id, arm, score) : unused arguments (id, arm, score)',fix:'Attach every package at the top, and write dplyr::select when the name is contested.'}};var ORDER=['state','path','seed','order'];var CHECKS=[{key:'state',label:'Runs in a session that knows nothing'},{key:'path',label:'Finds its data on another machine'},{key:'order',label:'Calls the function it means to call'},{key:'seed',label:'Gives the same answer twice'}];function mount(el,cfg){cfg=cfg||{};var want=(cfg.faults&&cfg.faults.length?cfg.faults:ORDER).filter(function(f){return FAULTS[f];});if(!want.length)want=ORDER.slice();var seed=(cfg.seed==null?12345:+cfg.seed);var lines=script(seed);var repaired={},open=null,runs=[];var P=palette(el);el.style.cssText='border:1px solid '+P.line+';border-radius:12px;background:'+P.panel+';padding:16px 17px';el.innerHTML='
    '+'analysis.R, exactly as it was sent to the journal
    '+'
    '+'Flagged lines are the faults. Click one to read it and apply the repair.
    '+'
    '+'
    '+'
    '+''+''+'
    '+'
    '+'
    '+'
    ';var codeBox=el.querySelector('.rp-code'),panel=el.querySelector('.rp-panel'),log=el.querySelector('.rp-log'),checks=el.querySelector('.rp-checks'),rbox=el.querySelector('.rp-r'),runBtn=el.querySelector('.rp-run'),resetBtn=el.querySelector('.rp-reset');codeBox.addEventListener('click',function(ev){var row=ev.target.closest?ev.target.closest('[data-f]'):null;if(!row||!codeBox.contains(row))return;var f=row.getAttribute('data-f');open=(open===f)?null:f;render();});panel.addEventListener('click',function(ev){var b=ev.target.closest?ev.target.closest('[data-fix]'):null;if(!b||!panel.contains(b))return;repaired[b.getAttribute('data-fix')]=true;open=null;runs=[];render();});runBtn.addEventListener('click',function(){runs=simulate(want,repaired,seed);render();});resetBtn.addEventListener('click',function(){repaired={};open=null;runs=[];render();});function render(){P=palette(el);el.style.background=P.panel;el.style.borderColor=P.line;codeBox.innerHTML=codeView(lines,want,repaired,open,P);panel.innerHTML=open?faultPanel(open,repaired,P):'';log.innerHTML=runs.length?logView(runs,P):'';checks.innerHTML=checkList(want,repaired,runs,P);rbox.innerHTML=u.runnable(rcode(seed),{label:'The repaired script, start to finish'});runBtn.style.background=P.acc;resetBtn.style.color=P.mut;resetBtn.style.borderColor=P.line;} var wasDark=isDark(el);if(window.MutationObserver){var mo=new MutationObserver(function(){if(!document.contains(el)){mo.disconnect();return;} var now=isDark(el);if(now!==wasDark){wasDark=now;render();}});mo.observe(document.documentElement,{attributes:true,attributeFilter:['class']});} render();} function codeView(lines,want,repaired,open,P){var h='
    ';var num=0,shown={};lines.forEach(function(L){var f=L.fault,active=f&&want.indexOf(f)>=0,done=active&&repaired[f];var text,bg='transparent',mark=' ';if(!active){text=L.t!=null?L.t:(L.fixed||L.broken);} else if(done){if(!L.fixed)return;text=L.fixed;bg=P.add;mark='+';}else{if(!L.broken)return;text=L.broken;bg=P.del;mark='-';} num++;if(active)shown[f]=true;var isOpen=active&&open===f;h+=''+''+num+''+''+mark+''+''+u.esc(text)+''+ (active&&!done?'fault':'')+'
    ';});var MISSING={seed:'missing: nothing sets a seed before the bootstrap',order:'missing: a package is attached too late',state:'missing: the object this script depends on',path:'missing: a portable path'};want.forEach(function(f){if(repaired[f]||shown[f])return;h+='
    '+' '+'!'+''+u.esc(MISSING[f]||'missing')+''+'fault'+'
    ';});return h+'
    ';} function faultPanel(f,repaired,P){var F=FAULTS[f],done=!!repaired[f];return'
    '+'
    '+u.esc(F.name)+'
    '+'
    '+F.why.replace(/`([^`]+)`/g,'$1')+'
    '+'
    The repair: '+u.esc(F.fix)+'
    '+ (done?'
    Repaired.
    ':'')+'
    ';} function simulate(want,repaired,seed){var out=[],pass=1,first=null;for(pass=1;pass<=2;pass++){var lines=['> Rscript analysis.R (fresh session '+pass+' of 2)'],stopped=false,k;for(k=0;k=0&&!repaired[f]){lines.push(FAULTS[f].err);out.push({ok:false,lines:lines,fault:f});stopped=true;break;}} if(stopped)return out;var noSeed=want.indexOf('seed')>=0&&!repaired.seed;var val=noSeed?bootValue(seed+pass*7717):bootValue(seed);lines.push('effect '+val.est+' 95% CI '+val.lo+' '+val.hi);out.push({ok:true,lines:lines,val:val});if(pass===1)first=val;else out.push({verdict:(first.est===val.est&&first.lo===val.lo&&first.hi===val.hi)});} return out;} function bootValue(s){function r(a){a|=0;a=a+0x6D2B79F5|0;var t=Math.imul(a^a>>>15,1|a);t=t+Math.imul(t^t>>>7,61|t)^t;return((t^t>>>14)>>>0)/4294967296;} var e=2.4+0.18*(r(s)-0.5),w=1.55+0.12*(r(s+1)-0.5);return{est:e.toFixed(3),lo:(e-w).toFixed(3),hi:(e+w).toFixed(3)};} function logView(runs,P){var h='
    ';runs.forEach(function(r){if(r.verdict!==undefined)return;r.lines.forEach(function(L,i){var col=i===0?'#7d8898':(r.ok?P.codeFg:P.bad);h+='
    '+u.esc(L)+'
    ';});h+='
    ';});h+='
    ';var v=runs.filter(function(r){return r.verdict!==undefined;})[0];var failed=runs.filter(function(r){return r.ok===false;})[0];if(failed){h+='
    '+'It stopped on the first fault it met. On your own machine this line works, because your session already holds what the script forgot to say.
    ';}else if(v&&!v.verdict){h+='
    '+'It ran twice with no error and gave two different answers. This is the fault that survives every code review, because nothing about it looks wrong. '+'The interval you would have published is one draw from a distribution nobody else can reach.
    ';}else if(v&&v.verdict){h+='
    '+'Two clean sessions, same numbers. That is what a reviewer means by reproducible: not that the analysis is right, but that it is the same analysis every time anyone runs it.
    ';} return h;} function checkList(want,repaired,runs,P){var ran=runs.length>0;var v=runs.filter(function(r){return r.verdict!==undefined;})[0];var done=0;var rows=CHECKS.filter(function(c){return want.indexOf(c.key)>=0;}).map(function(c){var ok=!!repaired[c.key]&&(c.key!=='seed'||(ran&&v&&v.verdict));if(ok)done++;return'
  • '+(ok?'x':'o')+''+u.esc(c.label)+'
  • ';});return'
    '+'What a fresh session has proved so far ('+done+' of '+rows.length+')
    '+'
      '+rows.join('')+'
    ';} function rcode(seed){return['# analysis.R, repaired. Everything it needs, it makes or loads itself.','set.seed('+seed+') # 1. the random step is now repeatable','','# 2. no absolute path: the data is built here rather than read from one laptop.','# In a real project this line is read.csv("data/trial.csv"), a path inside the','# project folder that travels with the project. Because the data is built here','# rather than read, these numbers are this script\'s own, not the console above.','n <- 300','raw <- data.frame(',' id = seq_len(n),',' arm = rep(c("control", "programme"), each = n / 2),',' score = rnorm(n, mean = rep(c(50, 52.4), each = n / 2), sd = 6)',')','raw$score[c(7, 19, 88)] <- NA # a little real-world mess','','# 3. no hidden state: every object below is created above.','trial <- raw[!is.na(raw$score), ]','keep <- trial[, c("id", "arm", "score")]','','# 4. no load-order surprise: base R here, and where a name is contested say','# which package you mean, as in dplyr::select(...).','boot <- replicate(2000, {',' i <- sample(nrow(keep), replace = TRUE)',' m <- tapply(keep$score[i], keep$arm[i], mean)',' unname(m["programme"] - m["control"])','})','','cat("effect", round(mean(boot), 3),',' " 95% CI", round(quantile(boot, c(.025, .975)), 3), "\\n")','','# Run it twice. Same numbers, on any machine, in any session.'].join('\n');} window.LessonWidgets.register('repro-repair',mount);})();;(function(){'use strict';function mount(el,cfg){var u=window.LessonWidgets.u;if(!u)return;cfg=cfg||{};var wide=cfg.wide||{cols:['country','2020','2021','2022'],rows:[['India',10,12,15],['Kenya',7,8,9]]};var idCols=cfg.idCols||['country'];var namesTo=cfg.namesTo||'year',valuesTo=cfg.valuesTo||'cases';var valCols=wide.cols.filter(function(c){return idCols.indexOf(c)<0;});var longCols=idCols.concat([namesTo,valuesTo]),longRows=[];wide.rows.forEach(function(r){var ids=idCols.map(function(c){return r[wide.cols.indexOf(c)];});valCols.forEach(function(vc){longRows.push(ids.concat([vc,r[wide.cols.indexOf(vc)]]));});});function wideHi(){var hi={};wide.rows.forEach(function(r,ri){wide.cols.forEach(function(c,ci){if(valCols.indexOf(c)>=0)hi[ri+','+ci]=u.P.line2;});});return hi;} function longHi(){var hi={};longRows.forEach(function(r,ri){hi[ri+','+(longCols.length-2)]=u.P.line2;hi[ri+','+(longCols.length-1)]=u.P.line2;});return hi;} var wrap=document.createElement('div');wrap.style.cssText='font-family:IBM Plex Sans,system-ui,sans-serif';wrap.innerHTML='
    '+'
    '+'
    '+'
    ';var segHost=wrap.querySelector('.rs-seg');segHost.innerHTML=u.seg([{v:'wide',label:'Wide'},{v:'long',label:'Long (tidy)'}],'wide');var stage=wrap.querySelector('.rs-stage'),cap=wrap.querySelector('.rs-cap'),codeEl=wrap.querySelector('.rs-code');function rsCode(form){if(form==='long'){return'library(tidyr)\n'+u.rdfCR('df',wide.cols,wide.rows)+'\n\n'+'pivot_longer(df, cols = -'+idCols.join(', -')+', names_to = "'+namesTo+'", values_to = "'+valuesTo+'")';} return'library(tidyr)\n'+u.rdfCR('long',longCols,longRows)+'\n\n'+'pivot_wider(long, names_from = '+namesTo+', values_from = '+valuesTo+')';} function render(form){if(form==='wide'){stage.innerHTML=u.tbl(wide.cols,wide.rows,{hi:wideHi()});cap.innerHTML='Wide: one row per '+idCols.join(', ')+', one column per '+namesTo+'. Easy to read, harder to plot. '+wide.rows.length+' rows.';}else{stage.innerHTML=u.tbl(longCols,longRows,{hi:longHi()});cap.innerHTML='Long (tidy): one row per observation, with '+namesTo+' and '+valuesTo+' columns. This is what ggplot and most models want. '+longRows.length+' rows.';} codeEl.innerHTML=u.runnable(rsCode(form),{label:'Run this reshape'});} u.wireSeg(segHost,function(v){render(v);});render('wide');el.innerHTML='';el.appendChild(wrap);} if(window.LessonWidgets)window.LessonWidgets.register('reshape-grid',mount);})();;(function(){'use strict';var u=window.LessonWidgets.u,P=u.P;var Z=[0.4,-1.1,0.7,-0.3,1.4,-0.8,0.2,0.9,-1.5,0.6,-0.2,1.1,-0.9,0.5,-1.3,0.8,-0.4,1.2,-0.7,0.3,-1.0,0.6,1.0,-0.6];function mount(el,cfg){cfg=cfg||{};var scen=cfg.start||'healthy';var N=Z.length;function fitted(i){return 2+8*i/(N-1);} function resid(i,s){var f=fitted(i),z=Z[i];if(s==='funnel')return z*(0.25+0.34*(f-2));if(s==='curved'){var c=(f-6);return 0.9*(c*c/4-1.4)+z*0.5;} return z*1.0;} el.style.cssText='border:1px solid '+P.line+';border-radius:12px;background:#fff;padding:16px 17px';el.innerHTML='
    '+u.seg([{v:'healthy',label:'Healthy fit'},{v:'funnel',label:'Funnel (non-constant variance)'},{v:'curved',label:'Curve (missed nonlinearity)'}],scen)+'
    '+'
    '+'
    '+ u.runnable(rcode(),{label:'See the real diagnostic plots in R'});var chart=el.querySelector('.rp-chart'),read=el.querySelector('.rp-read');function draw(){var W=480,H=250,m={t:16,r:14,b:36,l:44},iw=W-m.l-m.r,ih=H-m.t-m.b;var rs=[];for(var i=0;i';svg+='';svg+='';svg+='';var trend='';for(var t=0;t<=16;t++){var f=flo+(fhi-flo)*t/16;var acc=0,wsum=0;for(var j=0;j';for(i=0;i';svg+='fitted value →';svg+='residual';svg+='';chart.innerHTML=svg;var msg=scen==='funnel'?['Trouble: non-constant variance.','The spread fans out as the fitted value grows. The model is less certain for big predictions; standard errors are wrong. Fix with a transform or robust/weighted errors.']:scen==='curved'?['Trouble: a missed nonlinearity.','The residuals bend in a clear U instead of scattering flat. The straight-line model is leaving structure on the table; add a term or transform.']:['Healthy.','Residuals scatter in a flat, even band around zero with no pattern - exactly what the assumptions want. Constant variance, no missed curve.'];read.innerHTML=''+msg[0]+' '+msg[1];} u.wireSeg(el.querySelector('.rp-seg'),function(v){scen=v;draw();});draw();} function rcode(){return['# Residual diagnostics. Build a fit, then read its plots: a healthy model','# shows a flat, even band of residuals; a funnel or a curve is a warning.','set.seed(7)','x <- runif(60, 1, 10)','y <- 3 + 1.5 * x + rnorm(60, 0, 1.2) # a well-behaved linear relationship','fit <- lm(y ~ x)','','par(mfrow = c(1, 2))','plot(fit, which = 1) # residuals vs fitted (look for a flat band)','plot(fit, which = 2) # normal Q-Q (points on the line = normal)','par(mfrow = c(1, 1))'].join('\n');} window.LessonWidgets.register('residual-plot',mount);})();;(function(){'use strict';var u=window.LessonWidgets.u;var LIGHT={ink:'#131720',body:'#434b59',mut:'#677084',faint:'#97a0b2',line:'#d8dee9',grid:'#eef1f6',acc:'#1f7a55',c0:'#2563a8',c1:'#b5631a',bad:'#c2410c',panel:'#ffffff',soft:'#f6f8fa',pick:'#e7f3ec',warn:'#fbeae5'};var DARK={ink:'#eef4fb',body:'#c3d1e3',mut:'#93a4bb',faint:'#6f8299',line:'rgba(255,255,255,.24)',grid:'rgba(255,255,255,.10)',acc:'#46c08a',c0:'#7fb2ea',c1:'#e3a05a',bad:'#f4805a',panel:'#101c2b',soft:'rgba(255,255,255,.05)',pick:'rgba(70,192,138,.18)',warn:'rgba(244,128,90,.16)'};function lumOf(c){c=String(c).trim();var r,g,b,m;if(c.charAt(0)==='#'){if(c.length===4){r=parseInt(c[1]+c[1],16);g=parseInt(c[2]+c[2],16);b=parseInt(c[3]+c[3],16);} else{r=parseInt(c.substr(1,2),16);g=parseInt(c.substr(3,2),16);b=parseInt(c.substr(5,2),16);}}else if((m=c.match(/rgba?\(([^)]+)\)/))){var p=m[1].split(',');r=+p[0];g=+p[1];b=+p[2];} else return 1;return(0.2126*r+0.7152*g+0.0722*b)/255;} function isDark(el){try{var v=getComputedStyle(el).getPropertyValue('--lm-panel');if(v&&v.trim())return lumOf(v)<0.45;}catch(e){} return!!(document.documentElement&&document.documentElement.classList.contains('dark'));} function palette(el){return isDark(el)?DARK:LIGHT;} function mulberry32(a){return function(){a|=0;a=a+0x6D2B79F5|0;var t=Math.imul(a^a>>>15,1|a);t=t+Math.imul(t^t>>>7,61|t)^t;return((t^t>>>14)>>>0)/4294967296;};} var KINDS=[{k:'substantive',label:'Substantive',short:'the result could change',how:'Run it, report what it shows, and say so even if it goes against you.'},{k:'reporting',label:'Reporting',short:'the result stands, the write-up is thin',how:'Add the number or the definition, and say where it now appears.'},{k:'preference',label:'Preference',short:'a defensible difference of taste',how:'Give your reason, offer the alternative if it is cheap, and decline politely.'}];var DEFAULT_REVIEW=[{id:1,from:'Reviewer 2',kind:'substantive',text:'Participants were recruited in 14 clinics but the models treat every participant as independent. Outcomes within a clinic are likely correlated.',why:'Ignoring the clustering understates the standard errors, so the interval and the p-value both change. Nothing about this can be settled in the text.'},{id:2,from:'Reviewer 2',kind:'reporting',text:'Please state the software and package versions used for the analysis.',why:'A version number changes no estimate. It is missing from the manuscript, so it is a sentence in the methods, not a rerun.'},{id:3,from:'Reviewer 1',kind:'preference',text:'I would have used a Bayesian model with weakly informative priors rather than the frequentist approach taken here.',why:'Both approaches are defensible for this question, and the reviewer has not claimed the chosen one is invalid. Justify the choice and decline.'},{id:4,from:'Reviewer 1',kind:'substantive',text:'Forty-one participants are missing the primary outcome and appear to have been dropped. Were they different from those retained?',why:'If the missingness is related to the outcome, the complete-case estimate is biased. The answer is a comparison and a sensitivity analysis, and it might move the result.'},{id:5,from:'Reviewer 2',kind:'reporting',text:'Table 2 gives p-values but no effect sizes or confidence intervals.',why:'The numbers already exist in the fitted model. Reporting them changes nothing about the analysis and everything about whether a reader can judge it.'},{id:6,from:'Reviewer 3',kind:'preference',text:'Figure 3 would be clearer as a bar chart than as a boxplot.',why:'A boxplot shows the spread a bar chart hides, so this is a judgement call, and the reviewer has given no reason the current figure misleads.'},{id:7,from:'Reviewer 3',kind:'substantive',text:'The interaction reported in section 3.4 was not in the registered analysis plan and is not flagged as exploratory.',why:'An unregistered interaction found after looking is a different kind of evidence from a pre-specified one. The estimate stays; the CLAIM has to change, and that is substantive.'},{id:8,from:'Reviewer 1',kind:'reporting',text:'How was the primary outcome defined? The text refers to "improvement" without giving a threshold.',why:'The definition exists, or the analysis could not have been run. It is missing from the paper, so it goes in the methods.'},{id:9,from:'Reviewer 2',kind:'preference',text:'The introduction should cite Karlsson et al. (2019) and Ito (2021).',why:'Unless one of those papers contradicts the claim being made, this is a citation preference. Add them if they fit and say so in one line.'}];function mount(el,cfg){cfg=cfg||{};var comments=(cfg.comments&&cfg.comments.length?cfg.comments:DEFAULT_REVIEW).slice();var seed=(cfg.seed==null?5:+cfg.seed);if(cfg.shuffle!==false){var rnd=mulberry32(seed);for(var i=comments.length-1;i>0;i--){var j=Math.floor(rnd()*(i+1)),t=comments[i];comments[i]=comments[j];comments[j]=t;}} var picks={},graded=false;var P=palette(el);el.style.cssText='border:1px solid '+P.line+';border-radius:12px;background:'+P.panel+';padding:16px 17px';el.innerHTML='
    '+'Decision letter: major revision. Sort every comment before you answer one.
    '+'
    '+'
    '+'
    '+'
    '+'
    ';var legend=el.querySelector('.rt-legend'),list=el.querySelector('.rt-list'),actions=el.querySelector('.rt-actions'),score=el.querySelector('.rt-score'),rbox=el.querySelector('.rt-r');list.addEventListener('click',function(ev){var b=ev.target.closest?ev.target.closest('[data-pick]'):null;if(!b||!list.contains(b)||graded)return;picks[b.getAttribute('data-id')]=b.getAttribute('data-pick');render();});actions.addEventListener('click',function(ev){var b=ev.target.closest?ev.target.closest('[data-act]'):null;if(!b||!actions.contains(b))return;if(b.getAttribute('data-act')==='check')graded=true;else{graded=false;picks={};} render();});function render(){P=palette(el);el.style.background=P.panel;el.style.borderColor=P.line;el.querySelector('.rt-head').style.color=P.ink;legend.innerHTML=KINDS.map(function(K){return'
    '+''+K.label+''+u.esc(K.short)+'
    ';}).join('');list.innerHTML=comments.map(function(c){return card(c,picks[c.id],graded,P);}).join('');var answered=comments.filter(function(c){return picks[c.id];}).length;actions.innerHTML=graded?'':'';score.innerHTML=graded?summary(comments,picks,P):'';rbox.innerHTML=u.runnable(rcode(),{label:'What the first substantive comment actually costs'});} var wasDark=isDark(el);if(window.MutationObserver){var mo=new MutationObserver(function(){if(!document.contains(el)){mo.disconnect();return;} var now=isDark(el);if(now!==wasDark){wasDark=now;render();}});mo.observe(document.documentElement,{attributes:true,attributeFilter:['class']});} render();} function kindColour(k,P){return k==='substantive'?P.bad:(k==='reporting'?P.c0:P.mut);} function card(c,pick,graded,P){var right=graded&&pick===c.kind;var wrong=graded&&pick!==c.kind;var edge=graded?(right?P.acc:P.bad):P.line;var h='
    '+'
    '+ u.esc(c.from)+'
    '+'
    '+u.esc(c.text)+'
    '+'
    ';KINDS.forEach(function(K){var on=pick===K.k;var isAnswer=graded&&K.k===c.kind;var bg=graded?(isAnswer?P.pick:(on?P.warn:'transparent')):(on?P.pick:'transparent');var bd=graded?(isAnswer?P.acc:(on?P.bad:P.line)):(on?P.acc:P.line);h+='';});h+='
    ';if(graded){h+='
    '+''+(right?'Right: ':'It is '+kindLabel(c.kind)+'. ')+''+ u.esc(c.why)+'
    '+'
    '+ u.esc('In the letter: '+howFor(c.kind))+'
    ';} return h+'
    ';} function kindLabel(k){for(var i=0;i'+'
    '+ right+' of '+n+' sorted correctly
    ';h+='
    '+'This review is '+byKind.substantive+' substantive, '+byKind.reporting+' reporting and '+ byKind.preference+' preference. '+ (overCall?'You marked '+overCall+' as substantive that '+(overCall===1?'is not':'are not')+'. That is the expensive error: it turns a revision into a rebuild and invites the reviewer to keep pulling. ':'')+ (underCall?'You marked '+underCall+' substantive comment'+(underCall===1?'':'s')+' as something lighter. That is the dangerous error: answering a real problem with a sentence is what produces a hostile second round. ':'')+ (!overCall&&!underCall?'You put every genuinely substantive comment in the right pile, which is the part that decides how the second round goes. ':'')+'
    ';h+='
    The letter this implies
    '+'
      '+'
    1. Open with the '+byKind.substantive+' substantive points, each with what you ran and what it showed, including where it did not help you.
    2. '+'
    3. Group the '+byKind.reporting+' reporting points together and point to the line and page each one now appears on.
    4. '+'
    5. Take the '+byKind.preference+' preference points last, briefly, with a reason for the choice you made.
    6. '+'
    ';return h;} function rcode(){return['# Comment 1 said outcomes within a clinic are correlated. That is a substantive','# comment, so it costs a rerun. Here is the rerun, and what it changes.','set.seed(20)','k <- 14; m <- 20; n <- k * m # 14 clinics, 20 participants each','rho <- 0.06 # a small, very ordinary intraclass correlation','','clinic <- rep(1:k, each = m)','arm <- rep(rep(0:1, length.out = k), each = m) # the programme runs clinic-wide','y <- 0.35 * arm + sqrt(rho) * rnorm(k)[clinic] + sqrt(1 - rho) * rnorm(n)','','naive <- lm(y ~ arm)','se_n <- coef(summary(naive))["arm", "Std. Error"]','','# analyse at the level the treatment was assigned: one number per clinic','cm <- tapply(y, clinic, mean)','ca <- tapply(arm, clinic, max)','se_c <- sqrt(sum((cm - ave(cm, ca))^2) / (k - 2) * (2 / (k / 2)))','','round(c(naive_se = se_n,',' clustered_se = se_c,',' ratio = se_c / se_n,',' design_effect_formula = sqrt(1 + (m - 1) * rho)), 3)','','# The estimate barely moves. The standard error does, and so does the p-value.','round(c(naive_p = coef(summary(naive))["arm", "Pr(>|t|)"],',' clustered_p = unname(2 * pt(abs(diff(rev(tapply(cm, ca, mean)))) / se_c,',' df = k - 2, lower.tail = FALSE))), 4)'].join('\n');} window.LessonWidgets.register('review-triage',mount);})();;(function(){'use strict';var u=window.LessonWidgets.u,P=u.P;var D=(function(){var a=[],s=3;function r(){s=(s*1103515245+12345)&0x7fffffff;return s/0x7fffffff-0.5;} for(var i=0;i<14;i++){var x=1+i*0.55;a.push({x:x,y:2+1.4*x+r()*1.3});} a.push({x:7.2,y:2.0});return a;})();function fit(method){var w=D.map(function(){return 1;}),b0=0,b1=0,it,k;for(it=0;it<(method==='ols'?1:12);it++){var sw=0,swx=0,swy=0,swxx=0,swxy=0;D.forEach(function(d,i){var wi=w[i];sw+=wi;swx+=wi*d.x;swy+=wi*d.y;swxx+=wi*d.x*d.x;swxy+=wi*d.x*d.y;});var den=sw*swxx-swx*swx;b1=(sw*swxy-swx*swy)/den;b0=(swy-b1*swx)/sw;var res=D.map(function(d){return d.y-(b0+b1*d.x);});var mad=res.map(Math.abs).sort(function(a,b){return a-b;})[Math.floor(res.length/2)]*1.4826||1;if(method==='ols')break;k=method==='huber'?1.345:4.685;w=res.map(function(rr){var u2=rr/(mad*k);return method==='huber'?Math.min(1,1/Math.abs(u2||1e-9)):(Math.abs(u2)<1?(1-u2*u2)*(1-u2*u2):0);});} return{b0:b0,b1:b1,w:w};} function mount(el,cfg){cfg=cfg||{};var method='ols';el.style.cssText='border:1px solid '+P.line+';border-radius:12px;background:#fff;padding:16px 17px';el.innerHTML='
    '+u.seg([{v:'ols',label:'OLS'},{v:'huber',label:'Huber'},{v:'tukey',label:'Tukey'}],'ols')+'
    '+'
    '+'
    '+ u.runnable(rcode(),{label:'OLS vs a robust fit with MASS::rlm()'});var plot=el.querySelector('.rw-plot'),read=el.querySelector('.rw-read');var xs=D.map(function(d){return d.x;}),ys=D.map(function(d){return d.y;});var xe=[Math.min.apply(null,xs)-0.5,Math.max.apply(null,xs)+0.5],ye=[0,Math.max.apply(null,ys)+1];var W=440,H=250,m={l:40,r:12,t:12,b:30},iw=W-m.l-m.r,ih=H-m.t-m.b;function sx(x){return m.l+(x-xe[0])/(xe[1]-xe[0])*iw;} function sy(y){return m.t+ih-(y-ye[0])/(ye[1]-ye[0])*ih;} function draw(){var f=fit(method),fo=fit('ols');var svg='';svg+='';if(method!=='ols')svg+='';svg+='';D.forEach(function(d,i){var wt=method==='ols'?1:f.w[i];var out=d.x>6.9&&d.y<4;svg+='';});svg+='';plot.innerHTML=svg;var slope=f.b1.toFixed(2),olsSlope=fo.b1.toFixed(2),trueSlope='1.40';read.innerHTML=method==='ols'?'OLS slope '+slope+'. One outlier (red) drags the least-squares line well below the true trend ('+trueSlope+'): every point, however wrong, gets full weight.':''+(method==='huber'?'Huber':'Tukey')+' slope '+slope+' vs OLS '+olsSlope+' (dashed). The estimator gives the outlier a weight of '+f.w[D.length-1].toFixed(2)+', so the line snaps back to the honest slope near '+trueSlope+'. '+(method==='tukey'?'Tukey redescends to weight 0: gross outliers are rejected outright.':'Huber caps a big residual\'s pull without discarding it.');} u.wireSeg(el.querySelector('.rw-seg'),function(v){method=v;draw();});draw();} function rcode(){return['# One outlier is enough to tilt an OLS line. Robust regression down-weights it.','library(MASS)','set.seed(3)','x <- seq(1, 8, length.out = 14)','y <- 2 + 1.4 * x + rnorm(14, 0, 0.6)','x <- c(x, 7.2); y <- c(y, 2.0) # add one bad point (should be ~12, is 2)','','coef(lm(y ~ x))["x"] # OLS: slope dragged well below 1.4','coef(rlm(y ~ x, psi = psi.bisquare))["x"] # Tukey bisquare: back near the true 1.4','# rlm iteratively reweights: the outlier gets a near-zero weight and loses its pull.'].join('\n');} window.LessonWidgets.register('robust-weights',mount);})();;(function(){'use strict';var u=window.LessonWidgets.u,P=u.P;var POS=[0.92,0.86,0.8,0.74,0.7,0.64,0.6,0.54,0.46,0.4];var NEG=[0.6,0.5,0.46,0.4,0.34,0.3,0.25,0.2,0.14,0.1];function mount(el,cfg){cfg=cfg||{};var thr=0.5;function counts(t){var tp=0,fn=0,fp=0,tn=0;POS.forEach(function(s){(s>=t?tp++:fn++);});NEG.forEach(function(s){(s>=t?fp++:tn++);});return{tp:tp,fn:fn,fp:fp,tn:tn,tpr:tp/(tp+fn),fpr:fp/(fp+tn)};} var ROC=[];for(var t=1.02;t>=-0.02;t-=0.02){var c=counts(t);ROC.push([c.fpr,c.tpr]);} var auc=0;for(var i=1;i'+'
    '+''+''+'
    '+ u.runnable(rcode(),{label:'Build the ROC curve + AUC from scratch in R'});var roc=el.querySelector('.rc-roc'),cm=el.querySelector('.rc-cm'),read=el.querySelector('.rc-read'),slider=el.querySelector('.rc-s'),tEl=el.querySelector('.rc-t');function draw(){var c=counts(thr);var S=220,m=30,iw=S-m-10,ih=S-m-10;function px(f){return m+f*iw;}function py(tp){return(S-m)-tp*ih;} var line=ROC.map(function(p){return px(p[0]).toFixed(1)+','+py(p[1]).toFixed(1);}).join(' ');var svg='';svg+='';svg+='';svg+='';svg+='';svg+='false positive rate';svg+='true positive rate';svg+='AUC '+auc.toFixed(2)+'';svg+='';roc.innerHTML=svg;function cell(n,lab,good){return'
    '+n+'
    '+lab+'
    ';} cm.innerHTML='
    predicted ↓ / actual →
    '+'
    '+cell(c.tp,'true positive',true)+cell(c.fp,'false positive',false)+cell(c.fn,'false negative',false)+cell(c.tn,'true negative',true)+'
    '+'
    precision '+(c.tp/(c.tp+c.fp)||0).toFixed(2)+' · recall '+c.tpr.toFixed(2)+'
    ';read.innerHTML='At threshold '+thr.toFixed(2)+': recall (TPR) '+c.tpr.toFixed(2)+', false-positive rate '+c.fpr.toFixed(2)+'. Lowering it catches more positives but raises false alarms - you slide up the ROC curve. The curve (and its AUC) summarizes every threshold at once.';tEl.textContent=thr.toFixed(2);} slider.addEventListener('input',function(){thr=+slider.value;draw();});draw();} function rcode(){return['# An ROC curve sweeps EVERY threshold. Build it by hand from scores + labels.','pos <- c(0.92,0.86,0.8,0.74,0.7,0.64,0.6,0.54,0.46,0.4) # actual positives','neg <- c(0.6,0.5,0.46,0.4,0.34,0.3,0.25,0.2,0.14,0.1) # actual negatives','score <- c(pos, neg)','actual <- c(rep(1, length(pos)), rep(0, length(neg)))','','thr <- seq(0, 1, by = 0.02)','roc <- t(sapply(thr, function(t) {',' pred <- score >= t',' c(FPR = sum(pred & actual == 0) / sum(actual == 0),',' TPR = sum(pred & actual == 1) / sum(actual == 1))','}))','plot(roc[, "FPR"], roc[, "TPR"], type = "l", lwd = 2, xlab = "FPR", ylab = "TPR")','abline(0, 1, lty = 2)','','o <- order(roc[, "FPR"]) # area under the curve (trapezoid)','auc <- sum(diff(roc[o, "FPR"]) * (head(roc[o, "TPR"], -1) + tail(roc[o, "TPR"], -1)) / 2)','auc'].join('\n');} window.LessonWidgets.register('roc-curve',mount);})();;(function(){'use strict';function mount(el,cfg){var u=window.LessonWidgets.u;if(!u)return;cfg=cfg||{};var G=cfg.global||{x:10,y:20},L=cfg.local||{y:99};var names=Object.keys(G);Object.keys(L).forEach(function(k){if(names.indexOf(k)<0)names.push(k);});var wrap=document.createElement('div');wrap.style.cssText='font-family:IBM Plex Sans,system-ui,sans-serif';function envBox(title,vars,opts){opts=opts||{};var rows=Object.keys(vars).map(function(k){var hit=opts.foundKey===k,dim=opts.dimKey===k;var bg=hit?u.P.add:'#fff';return''+u.esc(k)+' = '+u.esc(vars[k])+(hit?' ✓':'')+'';}).join(' ');return'
    '+'
    '+u.esc(title)+'
    '+'
    '+(rows||'(empty)')+'
    '+'
    ';} function render(pick){var localHas=pick!=null&&Object.prototype.hasOwnProperty.call(L,pick);var lo={},go={},msg='';if(pick!=null){if(localHas){lo={active:true,foundKey:pick};go={};msg=''+pick+' is found in f()’s own environment → '+L[pick]+'. The local value shadows the global one.';} else{lo={active:true,dimKey:undefined};go={foundKey:pick};msg=''+pick+' is not in f(), so R looks outward to the global environment → '+G[pick]+'.';}} wrap.querySelector('.sc-local').innerHTML=envBox('inside f() (local)',L,lo);wrap.querySelector('.sc-global').innerHTML=envBox('global environment',G,go);wrap.querySelector('.sc-msg').innerHTML=pick==null?'Pick a name to resolve from inside f().':msg;} wrap.innerHTML='
    '+'Inside f(), look up:'+ names.map(function(nm){return'';}).join('')+'
    '+'
    '+'
    ↓ if not found, look outward
    '+'
    '+'
    '+'
    '+u.runnable('x <- 10\ny <- 20\n\nf <- function() {\n y <- 99\n c(x = x, y = y) # x from global, y from local\n}\n\nf()',{label:'Run it in R'})+'
    ';wrap.addEventListener('click',function(e){var b=e.target.closest('.sc-pick');if(!b)return;Array.prototype.forEach.call(wrap.querySelectorAll('.sc-pick'),function(x){x.style.background='#fff';x.style.color=u.P.mut;});b.style.background=u.P.acc;b.style.color='#fff';render(b.getAttribute('data-n'));});el.innerHTML='';el.appendChild(wrap);render(null);} if(window.LessonWidgets)window.LessonWidgets.register('scope-chain',mount);})();;(function(){'use strict';var u=window.LessonWidgets.u,P=u.P;var BASE=0.30;var CONTRIB=[['tenure',+0.22],['monthly spend',+0.14],['support calls',-0.18],['discount',+0.09],['contract: 1yr',-0.07]];function mount(el,cfg){cfg=cfg||{};var pred=BASE+CONTRIB.reduce(function(s,c){return s+c[1];},0);el.style.cssText='border:1px solid '+P.line+';border-radius:12px;background:#fff;padding:16px 17px';el.innerHTML='
    '+'
    The bars start at the baseline ('+BASE.toFixed(2)+', the average prediction) and each feature pushes the score up (blue) or down (amber). They sum to exactly this customer\'s prediction ('+pred.toFixed(2)+') - that is what makes SHAP an explanation, not just a ranking.
    '+ u.runnable(rcode(),{label:'Compute exact additive contributions in R'});var W=460,rowH=30,m=120,top=14,iw=W-m-60;var lo=Math.min(BASE,pred)-0.05,hi=Math.max(BASE,pred)+0.05;var rows=[],run=BASE;CONTRIB.forEach(function(c){rows.push({name:c[0],from:run,to:run+c[1],v:c[1]});run+=c[1];});function px(x){return m+(x-lo)/(hi-lo)*iw;} var H=top+rowH*(CONTRIB.length+2)+10;var svg='';function bandLabel(y,txt,val,bold){return''+txt+''+''+val+'';} var y=top+12;svg+='';svg+=bandLabel(y,'baseline',BASE.toFixed(2),true);y+=rowH;rows.forEach(function(r){var x1=px(Math.min(r.from,r.to)),x2=px(Math.max(r.from,r.to)),up=r.v>=0;svg+='';svg+=bandLabel(y,r.name,(up?'+':'')+r.v.toFixed(2),false);y+=rowH;});svg+='';svg+=bandLabel(y,'prediction',pred.toFixed(2),true);svg+='';el.querySelector('.sh-plot').innerHTML=svg;} function rcode(){return['# For a linear model, a feature\'s SHAP value has an exact closed form:','# contribution_j = beta_j * (x_j - mean(x_j))','# and the contributions + the baseline sum to the prediction.','set.seed(1)','d <- data.frame(tenure = rnorm(200), spend = rnorm(200), calls = rnorm(200))','d$y <- 0.3 + 0.8*d$tenure + 0.5*d$spend - 0.6*d$calls + rnorm(200, 0, 0.3)','fit <- lm(y ~ tenure + spend + calls, data = d)','','b <- coef(fit)[-1] # slopes','mu <- colMeans(d[, c("tenure","spend","calls")])','row <- d[1, c("tenure","spend","calls")]','contrib <- b * (unlist(row) - mu) # per-feature SHAP contributions','','baseline <- predict(fit, newdata = as.data.frame(t(mu)))','c(baseline = baseline, contrib,',' reconstructed = baseline + sum(contrib), # == the prediction',' prediction = predict(fit, newdata = row))'].join('\n');} window.LessonWidgets.register('shap-bars',mount);})();;(function(){'use strict';var u=window.LessonWidgets.u,P=u.P;function mount(el,cfg){cfg=cfg||{};var mode='covariate';el.style.cssText='border:1px solid '+P.line+';border-radius:12px;background:#fff;padding:16px 17px';el.innerHTML='
    '+u.seg([{v:'covariate',label:'Covariate: P(x)'},{v:'label',label:'Label: P(y)'},{v:'concept',label:'Concept: P(y|x)'}],'covariate')+'
    '+'
    '+'
    '+ u.runnable(rcode(),{label:'Which shift breaks the model? Base R'});var plot=el.querySelector('.sh-plot'),read=el.querySelector('.sh-read');var W=340,H=180,PAD=12,T=10,Bm=24;function px(x){return PAD+(x+4)/8*(W-2*PAD);} function bell(mu,sd,col,dash){var pts=[];for(var x=-4;x<=4;x+=0.1){var d=Math.exp(-0.5*((x-mu)/sd)*((x-mu)/sd));pts.push(px(x).toFixed(1)+','+(H-Bm-d*(H-T-Bm)).toFixed(1));}return'';} function sigmoid(k,col,dash){var pts=[];for(var x=-4;x<=4;x+=0.1){var s=1/(1+Math.exp(-k*x));pts.push(px(x).toFixed(1)+','+(H-Bm-s*(H-T-Bm)).toFixed(1));}return'';} function draw(){var body;if(mode==='covariate'){body=bell(0,1,P.c0)+bell(1.6,1,P.c1,true)+sigmoid(1.5,P.faint);}else if(mode==='concept'){body=sigmoid(1.5,P.c0)+sigmoid(-1.5,P.c1,true);}else{var bw=46,cx=W/2;function bar(x,h,col){return'';} body=bar(cx-150,0.5*(H-T-Bm),P.c0)+bar(cx-95,0.5*(H-T-Bm),P.c0)+ bar(cx+55,0.25*(H-T-Bm),P.c1)+bar(cx+110,0.75*(H-T-Bm),P.c1)+'train 50/50'+'test 25/75';} var axisLbl=mode==='label'?'class 0 class 1':(mode==='concept'?'feature x -> P(y=1|x)':'feature x');plot.innerHTML=''+ body+''+ (mode!=='label'?'traintest (dashed)':'')+''+axisLbl+'';read.innerHTML=mode==='covariate'?'Covariate shift: the feature distribution P(x) slides (test sits to the right), but the relationship P(y|x) (grey) is unchanged. A correctly specified model stays valid, accuracy need not drop.':mode==='concept'?'Concept shift: the relationship P(y|x) itself flips, train rises with x, test falls. The model is now systematically wrong (accuracy can fall below chance). This is the dangerous one.':'Label shift: the class base rate P(y) changes (50/50 to 25/75) while P(x|y) holds. Thresholds and calibration break even when the feature-to-label map is intact.';} u.wireSeg(el.querySelector('.sh-seg'),function(v){mode=v;draw();});draw();} function rcode(){return['# Which kind of distribution shift actually breaks a model? Base R.','set.seed(1)','n <- 2000','x <- rnorm(n); y <- rbinom(n, 1, plogis(1.5 * x)) # TRAIN: P(y=1|x) = plogis(1.5 x)','fit <- glm(y ~ x, binomial)','acc <- function(f, xx, yy) mean((predict(f, data.frame(x = xx), type = "response") > 0.5) == yy)','','x_cov <- rnorm(n, 1.5); y_cov <- rbinom(n, 1, plogis( 1.5 * x_cov)) # covariate: P(x) moves','x_con <- rnorm(n); y_con <- rbinom(n, 1, plogis(-1.5 * x_con)) # concept: P(y|x) flips','round(c(train = acc(fit, x, y),',' covariate = acc(fit, x_cov, y_cov), # P(y|x) intact -> model still fine',' concept = acc(fit, x_con, y_con)), # relationship changed -> model breaks',' 2)','#> train covariate concept','#> 0.72 0.86 0.25 # concept shift falls BELOW chance; covariate shift does not'].join('\n');} window.LessonWidgets.register('shift-types',mount);})();;(function(){'use strict';var u=window.LessonWidgets.u,P=u.P;var GRAND=50;var G=[{id:'A',n:3,raw:61},{id:'B',n:25,raw:53},{id:'C',n:4,raw:39},{id:'D',n:40,raw:51},{id:'E',n:2,raw:66},{id:'F',n:30,raw:48},{id:'G',n:5,raw:42},{id:'H',n:12,raw:55}];function mount(el,cfg){cfg=cfg||{};var lam=0.5;el.style.cssText='border:1px solid '+P.line+';border-radius:12px;background:#fff;padding:16px 17px';el.innerHTML='
    '+''+'
    no pooling (each clinic alone)complete pooling (one number)
    '+'
    '+ u.runnable(rcode(),{label:'Partial pooling with lme4::lmer()'});var plot=el.querySelector('.sp-plot'),read=el.querySelector('.sp-read'),slider=el.querySelector('.sp-s'),vEl=el.querySelector('.sp-v');var W=440,H=180,m={l:26,r:26,t:22,b:24},iw=W-m.l-m.r;var lo=34,hi=70;function sx(v){return m.l+(v-lo)/(hi-lo)*iw;} function est(g){var w=lam*(1-g.n/(g.n+8));return g.raw+(GRAND-g.raw)*(lam>=1?1:Math.min(1,w*2));} function draw(){var y0=m.t+20;var svg='';svg+='';[40,50,60].forEach(function(t){svg+=''+t+'';});svg+='grand mean';G.forEach(function(g){var e=est(g),r=3+Math.min(7,Math.sqrt(g.n));svg+='';svg+='';svg+='';});svg+='faint = raw mean; green = pooled estimate (size = sample size)';svg+='';plot.innerHTML=svg;vEl.textContent=lam.toFixed(2);var smallPull=Math.abs(est(G[4])-G[4].raw).toFixed(1),bigPull=Math.abs(est(G[3])-G[3].raw).toFixed(1);read.innerHTML=lam<0.05?'No pooling: every clinic is trusted on its own, so tiny clinics (n=2, n=3) swing far from the grand mean on almost no evidence.':lam>0.95?'Complete pooling: all clinics collapse to the single grand mean, throwing away every real between-clinic difference.':'Partial pooling. Clinic E (n=2) is pulled '+smallPull+' toward the mean while clinic D (n=40) barely moves ('+bigPull+'): small, noisy groups borrow strength from the whole, exactly what a random-effects model does automatically.';} slider.addEventListener('input',function(){lam=+slider.value;draw();});draw();} function rcode(){return['# Partial pooling: a mixed model shrinks noisy small-group means toward the average.','library(lme4)','set.seed(1)','clinic <- factor(rep(LETTERS[1:8], times = c(3,25,4,40,2,30,5,12)))','y <- rnorm(length(clinic), 50 + rep(c(8,3,-9,1,12,-2,-6,5),',' times = c(3,25,4,40,2,30,5,12)), 6)','','raw <- tapply(y, clinic, mean) # noisy raw means','pooled <- coef(lmer(y ~ 1 + (1 | clinic)))$clinic[,1] # shrunk estimates','round(rbind(raw, pooled), 1) # small groups move most'].join('\n');} window.LessonWidgets.register('shrinkage-pool',mount);})();;(function(){'use strict';var u=window.LessonWidgets.u,P=u.P;var D=(function(){var a=[],s=8;function r(){s=(s*1103515245+12345)&0x7fffffff;return s/0x7fffffff-0.5;} for(var i=0;i<60;i++){var x=i/59*10;a.push({x:x,y:Math.sin(x*0.9)*3+0.15*x+r()*1.5});} return a;})();function truth(x){return Math.sin(x*0.9)*3+0.15*x;} function mount(el,cfg){cfg=cfg||{};var k=6;el.style.cssText='border:1px solid '+P.line+';border-radius:12px;background:#fff;padding:16px 17px';el.innerHTML='
    '+''+'
    stiff (underfit)flexible (overfit)
    '+'
    '+ u.runnable(rcode(),{label:'A penalized smooth with mgcv::gam()'});var plot=el.querySelector('.ss-plot'),read=el.querySelector('.ss-read'),slider=el.querySelector('.ss-s'),vEl=el.querySelector('.ss-v');var W=440,H=250,m={l:34,r:12,t:12,b:28},iw=W-m.l-m.r,ih=H-m.t-m.b;var xe=[0,10],ye=[-5,6];function sx(x){return m.l+(x-xe[0])/(xe[1]-xe[0])*iw;} function sy(y){return m.t+ih-(y-ye[0])/(ye[1]-ye[0])*ih;} function smooth(x){var bw=6/(k-1);var sw=0,swy=0,swx=0,swxx=0,swxy=0;D.forEach(function(d){var w=Math.exp(-((d.x-x)*(d.x-x))/(2*bw*bw));sw+=w;swy+=w*d.y;swx+=w*d.x;swxx+=w*d.x*d.x;swxy+=w*d.x*d.y;});var den=sw*swxx-swx*swx;var b1=den===0?0:(sw*swxy-swx*swy)/den,b0=(swy-b1*swx)/sw;return b0+b1*x;} function draw(){var svg='';svg+='';D.forEach(function(d){svg+='';});var tp='';for(var i=0;i<=100;i++){var x=i/100*10;tp+=sx(x).toFixed(1)+','+sy(truth(x)).toFixed(1)+' ';} svg+='';var fp='';for(var j=0;j<=120;j++){var xx=j/120*10;fp+=sx(xx).toFixed(1)+','+sy(smooth(xx)).toFixed(1)+' ';} svg+='';svg+='dashed = true curve';svg+='';plot.innerHTML=svg;vEl.textContent=k;read.innerHTML=k<=3?'Underfit. With so few basis functions the smooth is nearly a straight line and misses the real bends: high bias.':k>=20?'Overfit. The smooth is now flexible enough to chase individual noisy points, wiggling far from the true curve: high variance.':'Good fit. The smooth tracks the true curve (dashed) without chasing noise. A GAM adds a penalty so it lands here on its own; you set k as the upper limit on wiggliness.';} slider.addEventListener('input',function(){k=+slider.value;draw();});draw();} function rcode(){return['# A GAM fits a smooth curve; the penalty picks the right wiggliness automatically.','library(mgcv)','set.seed(8); n <- 200','x <- runif(n, 0, 10)','y <- sin(x * 0.9) * 3 + 0.15 * x + rnorm(n, 0, 1.2)','','g <- gam(y ~ s(x, k = 20)) # k caps flexibility; the penalty does the rest','summary(g)$s.table # edf = effective degrees of freedom actually used','# edf near 1 means a line; a larger edf means real curvature. The penalty avoids overfit.'].join('\n');} window.LessonWidgets.register('spline-smoother',mount);})();;(function(){'use strict';var u=window.LessonWidgets.u,P=u.P;var ERR=[{k:'linear',v:0.542},{k:'poly',v:0.396},{k:'tree',v:0.423},{k:'stacked',v:0.391}];var WT=[{k:'linear',v:-0.08},{k:'poly',v:0.71},{k:'tree',v:0.29}];function mount(el,cfg){cfg=cfg||{};var view='err';el.style.cssText='border:1px solid '+P.line+';border-radius:12px;background:#fff;padding:16px 17px';el.innerHTML='
    '+u.seg([{v:'err',label:'Test error'},{v:'wt',label:'Blend weights'}],'err')+'
    '+'
    '+'
    '+ u.runnable(rcode(),{label:'Build the stack: 3 base learners, out-of-fold preds, a meta-learner'});var plot=el.querySelector('.st-plot'),read=el.querySelector('.st-read');var W=340,H=210;function draw(){var data=view==='err'?ERR:WT;var maxv=Math.max.apply(null,data.map(function(d){return Math.abs(d.v);}))*1.15;var n=data.length,bw=W/n*0.6,gap=W/n;var base=view==='err'?H-26:H/2;var bars=data.map(function(d,i){var cx=gap*i+gap/2,h=Math.abs(d.v)/maxv*(view==='err'?(H-40):(H/2-20));var y=d.v>=0?base-h:base,hh=h;var isBest=view==='err'&&d.k==='stacked';var col=isBest?P.acc:(d.v<0?P.bad:P.c0);return''+''+d.k+''+''+d.v.toFixed(view==='err'?3:2)+'';}).join('');var zline=view==='wt'?'':'';plot.innerHTML=''+zline+bars+'';read.innerHTML=view==='err'?'RMSE on held-out folds. The stacked model (0.391) edges below the best single learner, poly (0.396): the blend is never worse than its parts, and usually a little better.':'How the meta-learner blends the three. It leans hardest on poly (0.71) and tree (0.29), and gives linear a tiny negative weight, correcting where the others agree.';} u.wireSeg(el.querySelector('.st-seg'),function(v){view=v;draw();});draw();} function rcode(){return['# Stacking: cross-validate 3 base learners, then blend their out-of-fold preds.','library(rpart)','set.seed(1)','n <- 120; x <- runif(n, 0, 6); y <- sin(x) + 0.3*x + rnorm(n, 0, 0.4)','d <- data.frame(x, y)','K <- 5; fold <- sample(rep(1:K, length.out = n))','oof <- matrix(NA, n, 3); colnames(oof) <- c("linear","poly","tree")','for (k in 1:K) { # out-of-fold predictions only',' tr <- d[fold != k, ]; te <- which(fold == k)',' oof[te,1] <- predict(lm(y ~ x, tr), d[te,])',' oof[te,2] <- predict(lm(y ~ poly(x,3), tr), d[te,])',' oof[te,3] <- predict(rpart(y ~ x, tr), d[te,])','}','rmse <- function(p) sqrt(mean((d$y - p)^2))','base <- apply(oof, 2, rmse)','meta <- lm(d$y ~ oof) # the meta-learner learns the blend','round(c(base, stacked = rmse(predict(meta))), 3)','# stacked RMSE sits below every base learner: the blend wins.'].join('\n');} window.LessonWidgets.register('stacking-blend',mount);})();;(function(){'use strict';function comma(n){return n.toString().replace(/\B(?=(\d{3})+(?!\d))/g,',');} function fmt(v,f){if(v==null||isNaN(v))return v;var n=+v;if(f==='dollar')return'$'+comma(Math.round(n));if(f==='pct')return(Math.round(n*1000)/10)+'%';if(f==='comma')return comma(Math.round(n));if(f==='1dp')return n.toFixed(1);return v;} function mount(el,cfg){var u=window.LessonWidgets.u;if(!u)return;cfg=cfg||{};var cols=cfg.cols||['region','revenue','share'];var rows=cfg.rows||[['North',1234567,0.42],['South',876543,0.30],['West',812000,0.28]];var formats=cfg.formats||{revenue:'dollar',share:'pct'};var title=cfg.title||'Q4 revenue by region',note=cfg.note||'Source: internal finance, 2026.';var P=u.P;function raw(){var h='';cols.forEach(function(c){h+='';});h+='';rows.forEach(function(r){h+='';r.forEach(function(v){h+='';});h+='';});return h+'
    '+u.esc(c)+'
    '+u.esc(v)+'
    ';} function styled(){var h='
    ';h+='
    '+u.esc(title)+'
    ';h+='';cols.forEach(function(c){var rt=formats[c];h+='';});h+='';rows.forEach(function(r,ri){h+='';r.forEach(function(v,ci){var c=cols[ci],rt=formats[c];h+='';});h+='';});h+='
    '+u.esc(c)+'
    '+u.esc(fmt(v,rt))+'
    ';h+='
    '+u.esc(note)+'
    ';return h;} var wrap=document.createElement('div');wrap.style.cssText='font-family:IBM Plex Sans,system-ui,sans-serif';wrap.innerHTML='
    ';var segHost=wrap.querySelector('.st-seg');segHost.innerHTML=u.seg([{v:'raw',label:'Raw print'},{v:'gt',label:'Report table'}],'raw');var stage=wrap.querySelector('.st-stage');function render(v){stage.innerHTML=v==='gt'?styled():raw();} u.wireSeg(segHost,function(v){render(v);});render('raw');el.innerHTML='';el.appendChild(wrap);} if(window.LessonWidgets)window.LessonWidgets.register('styled-table',mount);})();;(function(){'use strict';var u=window.LessonWidgets.u,P=u.P;var TPRE=20,TPOST=10,TT=TPRE+TPOST,EFFECT=3;var W_TRUE=[0.5,0.33,0.17,0,0];function rng(seed){var s=seed>>>0;return function(){s=(s*1664525+1013904223)>>>0;return s/4294967296;};} function build(){var r=rng(4),spare=null;function rnorm(){if(spare!==null){var v=spare;spare=null;return v;}var a=Math.max(r(),1e-12),b=r(),m=Math.sqrt(-2*Math.log(a));spare=m*Math.sin(2*Math.PI*b);return m*Math.cos(2*Math.PI*b);} var donors=[];for(var j=0;j<5;j++){var arr=[],c=10;for(var t=0;t=TPRE?EFFECT:0));} return{synth:synth,treated:treated};} function mount(el,cfg){cfg=cfg||{};var view='traj';var d=build();el.style.cssText='border:1px solid '+P.line+';border-radius:12px;background:#fff;padding:16px 17px';el.innerHTML='
    '+u.seg([{v:'traj',label:'Trajectories'},{v:'gap',label:'Gap (effect)'}],'traj')+'
    '+'
    '+'
    '+ u.runnable(rcode(),{label:'Synthetic control: donor weights via optim, base R'});var plot=el.querySelector('.sc-plot'),read=el.querySelector('.sc-read');var W=340,H=200,PAD=10,T=10,Bm=22;function px(t){return PAD+t/(TT-1)*(W-2*PAD);} function draw(){var series,ylo,yhi,cutX=px(TPRE-0.5);if(view==='traj'){series=[d.treated,d.synth];var all=d.treated.concat(d.synth);ylo=Math.min.apply(null,all);yhi=Math.max.apply(null,all);} else{series=[d.treated.map(function(v,i){return v-d.synth[i];})];ylo=-1;yhi=EFFECT+2;} function py(y){return T+(yhi-y)/(yhi-ylo)*(H-T-Bm);} function path(a,col,dash){return'';} var svg='';svg+='';svg+='policy';if(view==='traj'){svg+=path(d.synth,P.c1,true)+path(d.treated,P.c0);svg+='treated';svg+='synthetic';}else{svg+='';svg+=path(series[0],P.bad);svg+='treated - synthetic';} svg+='time ->';plot.innerHTML=svg;read.innerHTML=view==='traj'?'The synthetic (dashed) is a weighted blend of donors tuned to track the treated unit before the policy. They match pre-policy, then split: the post-policy gap is the effect.':'The gap sits near zero before the policy (the synthetic is a good twin), then jumps to about '+EFFECT+' after: the estimated causal effect. A pre-period gap near zero is what licenses reading the post-period gap.';} u.wireSeg(el.querySelector('.sc-seg'),function(v){view=v;draw();});draw();} function rcode(){return['# Synthetic control: build a weighted donor blend that tracks the treated unit pre-policy. Base R.','set.seed(4)','Tpre <- 20; Tpost <- 10; Tt <- Tpre + Tpost','donors <- sapply(1:5, function(j) cumsum(rnorm(Tt, 0.1 * j, 1)) + 10) # 5 donor units','treated <- as.numeric(donors %*% c(0.5, 0.3, 0.2, 0, 0)) + rnorm(Tt, 0, 0.3)','treated[(Tpre + 1):Tt] <- treated[(Tpre + 1):Tt] + 3 # TRUE post-policy effect = 3','','pre <- 1:Tpre # fit weights on the pre-period only','loss <- function(th) { w <- exp(th)/sum(exp(th)); sum((treated[pre] - donors[pre, ] %*% w)^2) }','w <- local({ o <- optim(rep(0, 5), loss); exp(o$par)/sum(exp(o$par)) }) # simplex weights','synth <- as.numeric(donors %*% w)','round(w, 2)','#> [1] 0.50 0.33 0.17 0.00 0.00 # recovers the true donor mix, zeroes the rest','','round(c(true = 3,',' effect = mean(treated[(Tpre+1):Tt] - synth[(Tpre+1):Tt]),',' pre_gap = mean(treated[pre] - synth[pre])), 2)','#> true effect pre_gap','#> 3.00 3.00 0.01 # near-zero pre-gap licenses the post-period read'].join('\n');} window.LessonWidgets.register('synth-control',mount);})();;(function(){'use strict';function mount(el,cfg){var u=window.LessonWidgets.u;if(!u)return;cfg=cfg||{};var before=cfg.before||{cols:['name','dept','salary'],rows:[['Aarti','Sales',48],['Biju','Sales',61],['Chen','Eng',77],['Devi','Eng',52]]};var after=cfg.after||{cols:['name','dept','salary'],rows:[['Biju','Sales',61],['Chen','Eng',77],['Devi','Eng',52]]};var code=cfg.code||'df %>% filter(salary > 50)';var caption=cfg.caption||'filter() keeps only the rows where the condition is TRUE.';var addCols={},dropCols={};after.cols.forEach(function(c){if(before.cols.indexOf(c)<0)addCols[c]=1;});before.cols.forEach(function(c){if(after.cols.indexOf(c)<0)dropCols[c]=1;});var afterKeys={};after.rows.forEach(function(r){afterKeys[JSON.stringify(r)]=1;});var delRows={};before.rows.forEach(function(r,i){if(!afterKeys[JSON.stringify(r)])delRows[i]=1;});var nDel=Object.keys(delRows).length,nAddCol=Object.keys(addCols).length,nDropCol=Object.keys(dropCols).length,nAddRow=Math.max(0,after.rows.length-(before.rows.length-nDel));function ttCode(){var dt=/:=|setDT|\.SD|data\.table/.test(code);var lib=dt?'library(data.table)\nsetDTthreads(1)\n':'library(dplyr)\n';return lib+u.rdfCR('df',before.cols,before.rows)+'\n\n'+code;} var wrap=document.createElement('div');wrap.style.cssText='font-family:IBM Plex Sans,system-ui,sans-serif';wrap.innerHTML=u.runnable(ttCode(),{label:'Run this transform'})+'
    '+'See exactly what changed:'+''+''+'
    '+'
    ';var stage=wrap.querySelector('.tt-stage'),cap=wrap.querySelector('.tt-cap'),runB=wrap.querySelector('.tt-run'),resetB=wrap.querySelector('.tt-reset');function showBefore(){stage.innerHTML=u.tbl(before.cols,before.rows);cap.innerHTML='Before. '+before.rows.length+' rows × '+before.cols.length+' columns.';} function showAfter(){var cols=before.cols.concat(after.cols.filter(function(c){return before.cols.indexOf(c)<0;}));var rows=before.rows.map(function(r,i){var row=before.cols.map(function(c,ci){return r[ci];});after.cols.forEach(function(c){if(before.cols.indexOf(c)<0){var ai=after.cols.indexOf(c);var match=after.rows.find(function(ar){return before.cols.every(function(bc,bi){return ar[after.cols.indexOf(bc)]===r[bi];});});row.push(match?match[ai]:null);}});return row;});stage.innerHTML=u.tbl(cols,rows,{addCols:addCols,dropCols:dropCols,delRows:delRows});var d=[];if(nDel)d.push('−'+nDel+' row'+(nDel>1?'s':''));if(nAddRow)d.push('+'+nAddRow+' row'+(nAddRow>1?'s':''));if(nAddCol)d.push('+'+nAddCol+' column'+(nAddCol>1?'s':''));if(nDropCol)d.push('−'+nDropCol+' column'+(nDropCol>1?'s':''));cap.innerHTML='After. '+caption+(d.length?' '+d.join(', ')+'':'');} showBefore();runB.addEventListener('click',function(){showAfter();runB.style.display='none';resetB.style.display='';});resetB.addEventListener('click',function(){showBefore();resetB.style.display='none';runB.style.display='';});el.innerHTML='';el.appendChild(wrap);} if(window.LessonWidgets)window.LessonWidgets.register('table-transform',mount);})();;(function(){'use strict';var PALS={default:{label:'default',colors:['#1f7a55','#2563a8','#b5631a','#7a5ea3','#2f8f86'],code:'scale_fill_brewer()'},cb:{label:'colorblind-safe',colors:['#E69F00','#56B4E9','#009E73','#0072B2','#D55E00'],code:'scale_fill_manual(values = okabe_ito) # colorblind-safe'},gray:{label:'grayscale',colors:['#222','#555','#777','#999','#bbb'],code:'scale_fill_grey()'}};var THEMES={minimal:{label:'theme_minimal',panel:'#ffffff',ink:'#131720'},gray:{label:'theme_gray',panel:'#ebedf0',ink:'#131720'},dark:{label:'theme_dark',panel:'#1f2430',ink:'#e6edf3'}};function mount(el,cfg){var u=window.LessonWidgets.u;if(!u)return;cfg=cfg||{};var data=cfg.data||[{x:'North',y:38},{x:'South',y:52},{x:'East',y:27},{x:'West',y:45},{x:'Central',y:33}];var xlab=cfg.x||'region',ylab=cfg.y||'sales';var pal='default',theme='minimal';var wrap=document.createElement('div');wrap.style.cssText='font-family:IBM Plex Sans,system-ui,sans-serif';wrap.innerHTML='
    '+'
    Palette
    '+'
    Theme
    '+'
    '+'
    ';var palHost=wrap.querySelector('.ts-pal');palHost.innerHTML=u.seg(Object.keys(PALS).map(function(k){return{v:k,label:PALS[k].label};}),pal);var thHost=wrap.querySelector('.ts-theme');thHost.innerHTML=u.seg(Object.keys(THEMES).map(function(k){return{v:k,label:THEMES[k].label};}),theme);var codeEl=wrap.querySelector('.ts-code');function runnableCode(){var p=PALS[pal],t=THEMES[theme];var pre='library(ggplot2)\n'+u.rdf(data,[{name:xlab,key:'x'},{name:ylab,key:'y'}])+'\n';if(pal==='cb')pre+='okabe_ito <- c("#E69F00", "#56B4E9", "#009E73", "#0072B2", "#D55E00")\n';return pre+'\nggplot(df, aes('+xlab+', '+ylab+', fill = '+xlab+')) +\n geom_col() +\n '+p.code+' +\n '+t.label+'()';} function render(){var t=THEMES[theme],p=PALS[pal];codeEl.innerHTML=u.runnable(runnableCode(),{label:'Run this chart'});var po=codeEl.querySelector('.webr-plot-output');if(po){po.style.background=t.panel;po.innerHTML=u.previewSeed(u.plot(data,{geom:'bar',x:xlab,y:ylab,palette:p.colors}));po.classList.add('has-content');}} u.wireSeg(palHost,function(v){pal=v;render();});u.wireSeg(thHost,function(v){theme=v;render();});render();el.innerHTML='';el.appendChild(wrap);} if(window.LessonWidgets)window.LessonWidgets.register('theme-styler',mount);})();;(function(){'use strict';var u=window.LessonWidgets.u,P=u.P;var RAW=(function(){var s=11,out=[];function r(){s=(s*1103515245+12345)&0x7fffffff;return s/0x7fffffff;}for(var i=0;i<240;i++)out.push(-Math.log(1-r())*2+0.05);return out;})();function mount(el,cfg){cfg=cfg||{};var mode='none';function tx(v){return mode==='log'?Math.log(v):mode==='sqrt'?Math.sqrt(v):mode==='box'?(Math.pow(v,0.25)-1)/0.25:v;} function skew(a){var n=a.length,m=a.reduce(function(s,x){return s+x;},0)/n,sd=Math.sqrt(a.reduce(function(s,x){return s+(x-m)*(x-m);},0)/n);return a.reduce(function(s,x){return s+Math.pow((x-m)/sd,3);},0)/n;} el.style.cssText='border:1px solid '+P.line+';border-radius:12px;background:#fff;padding:16px 17px';el.innerHTML='
    '+'
    '+'
    '+ u.runnable(rcode(),{label:'Transform a skewed variable in R and compare'});var seg=el.querySelector('.ts-seg'),plot=el.querySelector('.ts-plot'),read=el.querySelector('.ts-read');[['none','raw'],['log','log'],['sqrt','sqrt'],['box','Box-Cox']].forEach(function(o){var b=document.createElement('button');b.textContent=o[1];b.style.cssText='font:600 12px IBM Plex Sans,sans-serif;border:1px solid '+P.line+';border-radius:8px;padding:6px 12px;cursor:pointer;background:'+(o[0]===mode?P.ink:'#fff')+';color:'+(o[0]===mode?'#fff':P.body);b.addEventListener('click',function(){mode=o[0];draw();seg.querySelectorAll('button').forEach(function(x,i){var on=[['none'],['log'],['sqrt'],['box']][i][0]===mode;x.style.background=on?P.ink:'#fff';x.style.color=on?'#fff':P.body;});});seg.appendChild(b);});function draw(){var vals=RAW.map(tx),lo=Math.min.apply(null,vals),hi=Math.max.apply(null,vals),nb=16,bins=new Array(nb).fill(0);vals.forEach(function(v){var i=Math.min(nb-1,Math.floor((v-lo)/(hi-lo)*nb));bins[i]++;});var mx=Math.max.apply(null,bins),W=420,H=150,bw=W/nb;var svg='';bins.forEach(function(c,i){var h=c/mx*(H-20);svg+='';});svg+='';plot.innerHTML=svg;var sk=skew(vals);read.innerHTML='Skewness '+sk.toFixed(2)+' (0 = symmetric). '+ (Math.abs(sk)<0.4?'The transform has pulled the long tail in - now a linear model and distance metrics behave.':'Still skewed: the long right tail makes means and distances misleading. Try a transform.');} draw();} function rcode(){return['# A right-skewed feature (a long tail) distorts linear models and distances.','# A log or Box-Cox transform pulls the tail in toward symmetry.','set.seed(1)','x <- rexp(500, rate = 0.5) # heavily right-skewed','g <- function(v) mean(((v - mean(v)) / sd(v))^3) # skewness','','c(raw = g(x), log = g(log(x)), sqrt = g(sqrt(x))) # closer to 0 = more symmetric','par(mfrow = c(1, 2)); hist(x, main = "raw"); hist(log(x), main = "log")'].join('\n');} window.LessonWidgets.register('transform-shaper',mount);})();;(function(){'use strict';var INK='#131720',C0='#2563a8',C1='#b5631a',LINE='#c5cdda',MUT='#677084';function box(x,y,w,h,fill,stroke,text,tcolor,fs){return''+''+text+'';} function edge(x1,y1,x2,y2,label){var mx=(x1+x2)/2,my=(y1+y2)/2;return''+ (label?''+label+'':'');} function mount(el,cfg){cfg=cfg||{};var root=cfg.root||'tenure < 8 mo?';var l=cfg.l||'calls > 3?',r=cfg.r||'spend > $70?';var lv=cfg.leaves||['churns','stays','stays','churns'];var svg=''+ edge(230,49,120,116,'yes')+edge(230,49,340,116,'no')+ edge(120,144,70,214,'yes')+edge(120,144,170,214,'no')+ edge(340,144,290,214,'yes')+edge(340,144,390,214,'no')+ box(230,34,150,30,'#fff',INK,root,INK,12.5)+ box(120,130,120,28,'#fff','#9aa6b8',l,INK,11.5)+ box(340,130,120,28,'#fff','#9aa6b8',r,INK,11.5)+ leaf(70,228,lv[0])+leaf(170,228,lv[1])+leaf(290,228,lv[2])+leaf(390,228,lv[3])+'';el.innerHTML='
    '+svg+'
    ';} function leaf(x,y,label){var churn=/churn/i.test(label);var fill=churn?'rgba(181,99,26,0.12)':'rgba(37,99,168,0.12)';var stroke=churn?C1:C0;return box(x,y,86,28,fill,stroke,label,churn?C1:C0,11.5);} if(window.LessonWidgets)window.LessonWidgets.register('tree-diagram',mount);})();;(function(){'use strict';var u=window.LessonWidgets.u,P=u.P;function loss(a,b){return 0.2+0.8*(Math.pow(a-0.62,2)+Math.pow(b-0.38,2))+0.05*Math.sin(6*a)*Math.cos(5*b);} function mount(el,cfg){cfg=cfg||{};var mode='grid',N=16;el.style.cssText='border:1px solid '+P.line+';border-radius:12px;background:#fff;padding:16px 17px';el.innerHTML='
    '+'
    '+'
    '+ u.runnable(rcode(),{label:'Grid-search two hyperparameters in R'});var seg=el.querySelector('.tg-seg'),plot=el.querySelector('.tg-plot'),read=el.querySelector('.tg-read');[['grid','grid search'],['random','random search']].forEach(function(o){var b=document.createElement('button');b.textContent=o[1];b.style.cssText='font:600 12px IBM Plex Sans,sans-serif;border:1px solid '+P.line+';border-radius:8px;padding:6px 12px;cursor:pointer';b.addEventListener('click',function(){mode=o[0];draw();});seg.appendChild(b);});function pts(){var out=[];if(mode==='grid'){var k=Math.round(Math.sqrt(N));for(var i=0;i';for(var i=0;i';} var P2=pts(),best=null;P2.forEach(function(p){var lv=loss(p[0],p[1]);if(!best||lv';});svg+='';svg+='';plot.innerHTML=svg;read.innerHTML=mode==='grid'?'A grid spends its '+N+' evaluations on a regular lattice - so it only ever tries '+Math.round(Math.sqrt(N))+' distinct values of each parameter.':'Random search spends the same '+N+' evaluations on '+N+' distinct values per axis, so it usually finds a better spot when one parameter barely matters.';} draw();} function rcode(){return['# Score every combination on a validation set, then keep the best.','val_loss <- function(a, b) 0.2 + (a - 0.62)^2 + (b - 0.38)^2 # stand-in for CV error','grid <- expand.grid(param1 = seq(0, 1, length.out = 5),',' param2 = seq(0, 1, length.out = 5))','grid$loss <- mapply(val_loss, grid$param1, grid$param2)','grid[which.min(grid$loss), ] # the best combination','','# random search: same budget, different values per axis','set.seed(1); rs <- data.frame(param1 = runif(25), param2 = runif(25))','rs$loss <- mapply(val_loss, rs$param1, rs$param2)','rs[which.min(rs$loss), ]'].join('\n');} window.LessonWidgets.register('tuning-search',mount);})();;(function(){'use strict';var u=window.LessonWidgets.u,P=u.P;function rng(seed){var s=seed>>>0;return function(){s=(s*1664525+1013904223)>>>0;return s/4294967296;};} function plogis(z){return 1/(1+Math.exp(-z));} function build(){var r=rng(5);var units=[];for(var i=0;i<4000;i++){var x=r()*2-1,treat=r()<0.5?1:0;var tau=0.35*x+0.15;var p=plogis(-0.2+0.5*x)+treat*tau;var y=r()'+'
    '+'
    '+ u.runnable(rcode(),{label:'T-learner uplift model + quartile validation, base R'});var plot=el.querySelector('.up-plot'),read=el.querySelector('.up-read');var W=340,H=200,PADL=30,PADR=8,T=10,Bm=24;function px(f){return PADL+f*(W-PADL-PADR);} function py(q){return T+(qmax-q)/(qmax+1e-9)*(H-T-Bm);} function path(a,col,dash){return'';} function draw(){var sel=mode==='model'?qModel:qRand,other=mode==='model'?qRand:qModel;var peak=qModel[qModel.length-1].q,best=qmax;plot.innerHTML=''+''+ path(other,P.line2,true)+path(sel,mode==='model'?P.acc:P.mut)+'cumulative incremental conversions'+'fraction of population targeted ->';read.innerHTML=mode==='model'?'Ranking by predicted uplift and treating from the top bends the Qini curve above the random diagonal: the same budget wins far more incremental conversions. The curve peaks before 100%, past there you are treating people the offer HURTS.':'Random targeting traces the straight diagonal: incremental conversions grow in proportion to how many you treat, with no benefit from ordering. The gap to the uplift curve is what targeting buys.';} u.wireSeg(el.querySelector('.up-seg'),function(v){mode=v;draw();});draw();} function rcode(){return['# Uplift (heterogeneous effects): who does the treatment actually help? Base R T-learner.','set.seed(5)','n <- 4000; x <- runif(n, -1, 1); treat <- rbinom(n, 1, 0.5) # randomized','tau <- 0.35 * x + 0.15 # TRUE per-unit uplift varies with x','y <- rbinom(n, 1, pmin(pmax(plogis(-0.2 + 0.5*x) + treat*tau, 0), 1))','','m1 <- glm(y ~ x, binomial, subset = treat == 1) # T-learner: one model per arm','m0 <- glm(y ~ x, binomial, subset = treat == 0)','uplift <- predict(m1, data.frame(x), type="response") - predict(m0, data.frame(x), type="response")','','# validate: actual treated-minus-control response within predicted-uplift quartiles','q <- cut(uplift, quantile(uplift, 0:4/4), labels = 1:4, include.lowest = TRUE)','round(sapply(1:4, function(g) mean(y[q==g & treat==1]) - mean(y[q==g & treat==0])), 2)','#> [1] -0.09 0.02 0.26 0.39 # rises with predicted uplift; Q1 is HURT by treatment','round(mean(y[treat==1]) - mean(y[treat==0]), 2) # the flat average that hides all this','#> [1] 0.15'].join('\n');} window.LessonWidgets.register('uplift-curve',mount);})();;(function(){'use strict';var RANK={logical:1,integer:2,double:3,character:4};function mount(el,cfg){var u=window.LessonWidgets.u;if(!u)return;cfg=cfg||{};var start=cfg.start||[{lit:'TRUE',type:'logical'},{lit:'2L',type:'integer'}];var ADD=[{lit:'TRUE',type:'logical',label:'TRUE'},{lit:'7L',type:'integer',label:'7L (integer)'},{lit:'3.5',type:'double',label:'3.5 (double)'},{lit:'"hi"',type:'character',label:'"hi" (text)'}];var items=start.slice();function targetType(){var r=1;items.forEach(function(it){r=Math.max(r,RANK[it.type]);});return Object.keys(RANK).filter(function(k){return RANK[k]===r;})[0];} function coerce(it,t){if(t==='character'){var s=it.lit.replace(/^"|"$/g,'').replace(/L$/,'');return'"'+s+'"';} if(t==='double'||t==='integer'){if(it.type==='logical')return it.lit==='TRUE'?'1':'0';return it.lit.replace(/L$/,'');} return it.lit;} function rLiteral(){return'c('+items.map(function(it){return it.lit;}).join(', ');} var wrap=document.createElement('div');wrap.style.cssText='font-family:IBM Plex Sans,system-ui,sans-serif';function render(){var t=targetType();var coerced=items.map(function(it){return coerce(it,t);});var pieces=items.map(function(it){var changed=coerce(it,t)!==it.lit;return''+u.esc(it.lit)+' '+it.type+'';}).join(', ');var bg={logical:'#eef3f9',integer:'#eef3f9',double:'#e7f3ec',character:'#fbeae5'}[t];wrap.querySelector('.vc-in').innerHTML='c( '+pieces+' )';wrap.querySelector('.vc-out').innerHTML='
    R stores them all as one type:
    '+''+'[1] '+coerced.join(' ')+''+'
    typeof(x) #> "'+t+'"
    ';var run=wrap.querySelector('.vc-run');run.innerHTML=u.runnable('x <- '+rLiteral()+')\nx\ntypeof(x)',{label:'Run it in R'});} wrap.innerHTML='
    '+'
    Your vector
    '+'
    '+'
    '+'Add an element:'+ ADD.map(function(a,i){return'';}).join('')+''+'
    '+'
    '+'
    '+'
    ';wrap.addEventListener('click',function(e){var add=e.target.closest('.vc-add'),rs=e.target.closest('.vc-reset');if(add){items.push(ADD[+add.getAttribute('data-i')]);render();} else if(rs){items=start.slice();render();}});el.innerHTML='';el.appendChild(wrap);render();} if(window.LessonWidgets)window.LessonWidgets.register('vector-coercion',mount);})();;(function(){'use strict';var u=window.LessonWidgets.u,P=u.P;var DATA={erm:{A:0.96,B:0.11,worst:0.11,avg:0.88},dro:{A:0.71,B:0.66,worst:0.66,avg:0.70}};var BARS=[{k:'A',label:'group A (majority)'},{k:'B',label:'group B (minority)'},{k:'worst',label:'worst group'},{k:'avg',label:'average'}];function mount(el,cfg){cfg=cfg||{};var method='erm';el.style.cssText='border:1px solid '+P.line+';border-radius:12px;background:#fff;padding:16px 17px';el.innerHTML='
    '+u.seg([{v:'erm',label:'ERM (average loss)'},{v:'dro',label:'DRO (worst group)'}],'erm')+'
    '+'
    '+'
    '+ u.runnable(rcode(),{label:'ERM vs group-reweighted DRO: worst-group accuracy, base R'});var plot=el.querySelector('.wg-plot'),read=el.querySelector('.wg-read');var W=340,H=190,PADL=10,PADR=10,T=14,Bm=40;function draw(){var d=DATA[method],n=BARS.length,gap=(W-PADL-PADR)/n,bw=gap*0.55;var bars=BARS.map(function(b,i){var v=d[b.k],x=PADL+i*gap+(gap-bw)/2,h=v*(H-T-Bm);var col=b.k==='worst'?P.bad:(b.k==='avg'?P.mut:P.c0);return''+''+v.toFixed(2)+''+''+b.k+'';}).join('');plot.innerHTML=''+''+ bars+'accuracy per group (A majority, B minority)';read.innerHTML=method==='erm'?'ERM minimizes average loss and posts a healthy 0.88 overall, but group B (the minority, where a spurious feature reverses) sits at 0.11, worse than a coin flip. The average hid a group in freefall.':'DRO upweights the failing group, lifting the worst group from 0.11 to 0.66. Average accuracy drops from 0.88 to 0.70: the deliberate price of protecting the worst-off group.';} u.wireSeg(el.querySelector('.wg-seg'),function(v){method=v;draw();});draw();} function rcode(){return['# Worst-group accuracy: ERM can pass on average and fail a minority group. Base R.','set.seed(4)','n <- 3000','grp <- ifelse(runif(n) < 0.9, "A", "B") # A majority 90%, B minority 10%','core <- rnorm(n); y <- rbinom(n, 1, plogis(1.2 * core)) # the TRUE signal','spur <- ifelse(grp == "A", 2*y - 1, 1 - 2*y) + rnorm(n, 0, 0.5) # spurious: reversed for B','d <- data.frame(y, core, spur, grp)','gacc <- function(fit) { p <- predict(fit, d, type = "response") > 0.5',' aA <- mean(p[grp=="A"]==y[grp=="A"]); aB <- mean(p[grp=="B"]==y[grp=="B"])',' c(A = aA, B = aB, worst = min(aA, aB), avg = mean(p == y)) }','','erm <- glm(y ~ core + spur, binomial, data = d) # minimize AVERAGE loss','dro <- glm(y ~ core + spur, binomial, data = d,',' weights = ifelse(grp == "B", 9, 1)) # upweight the minority (DRO)','round(rbind(ERM = gacc(erm), DRO = gacc(dro)), 2)','#> A B worst avg','#> ERM 0.96 0.11 0.11 0.88 # great average, minority in freefall','#> DRO 0.71 0.66 0.66 0.70 # worst group rescued, small average cost'].join('\n');} window.LessonWidgets.register('worst-group',mount);})();;(function(){'use strict';var u=window.LessonWidgets.u;var LIGHT={ink:'#131720',body:'#434b59',mut:'#677084',faint:'#97a0b2',line:'#d8dee9',grid:'#eef1f6',acc:'#1f7a55',c0:'#2563a8',c1:'#b5631a',bad:'#c2410c',panel:'#ffffff',soft:'#f6f8fa',warn:'#fbeae5'};var DARK={ink:'#eef4fb',body:'#c3d1e3',mut:'#93a4bb',faint:'#6f8299',line:'rgba(255,255,255,.24)',grid:'rgba(255,255,255,.10)',acc:'#46c08a',c0:'#7fb2ea',c1:'#e3a05a',bad:'#f4805a',panel:'#101c2b',soft:'rgba(255,255,255,.05)',warn:'rgba(244,128,90,.16)'};function lumOf(c){c=String(c).trim();var r,g,b,m;if(c.charAt(0)==='#'){if(c.length===4){r=parseInt(c[1]+c[1],16);g=parseInt(c[2]+c[2],16);b=parseInt(c[3]+c[3],16);} else{r=parseInt(c.substr(1,2),16);g=parseInt(c.substr(3,2),16);b=parseInt(c.substr(5,2),16);}}else if((m=c.match(/rgba?\(([^)]+)\)/))){var p=m[1].split(',');r=+p[0];g=+p[1];b=+p[2];} else return 1;return(0.2126*r+0.7152*g+0.0722*b)/255;} function isDark(el){try{var v=getComputedStyle(el).getPropertyValue('--lm-panel');if(v&&v.trim())return lumOf(v)<0.45;}catch(e){} return!!(document.documentElement&&document.documentElement.classList.contains('dark'));} function palette(el){return isDark(el)?DARK:LIGHT;} function mulberry32(a){return function(){a|=0;a=a+0x6D2B79F5|0;var t=Math.imul(a^a>>>15,1|a);t=t+Math.imul(t^t>>>7,61|t)^t;return((t^t>>>14)>>>0)/4294967296;};} function gaussian(rnd){var a=1-rnd(),b=rnd();return Math.sqrt(-2*Math.log(a))*Math.cos(2*Math.PI*b);} function rpois(rnd,lam){var L=Math.exp(-lam),k=0,p=1;do{k++;p*=rnd();}while(p>L);return k-1;} function logistic(z){return 1/(1+Math.exp(-z));} function lsq(X,y){var n=X.length,p=X[0].length,cols=[],Q=[],R=[],i,j,k;for(j=0;j1e-11)for(i=0;i=0;j--){var t=b[j];for(k=j+1;k1e-11?t/R[j][j]:0;} return beta;} function glm(X,y,family){var n=X.length,p=X[0].length,beta=new Float64Array(p),it,i,j;for(it=0;it<60;it++){var Xw=[],z=new Float64Array(n);for(i=0;i1)bad++;if(fworstHi)worstHi=f;});return{x:x,y:y,lin:lin,log:log,bad:bad,worstLo:worstLo,worstHi:worstHi,n:n,outX:4,outLin:lin[0]+lin[1]*4,outLog:logistic(log[0]+log[1]*4)};}},'zero-inflation':{short:'Zero inflation',n:260,title:'Counts with a pile of structural zeros, fitted as ordinary Poisson',build:function(n,seed){var rnd=mulberry32(seed),x=[],y=[],i,pZero=0.45;for(i=0;imaxC)maxC=y[i];} return{x:x,y:y,b:b,obsZero:obsZero,expZero:expZero,maxC:maxC,n:n,pZero:pZero};}},ordinal:{short:'Ordinal as numeric',n:240,title:'A 1-to-5 rating treated as a number',build:function(n,seed){var rnd=mulberry32(seed),cuts=[-2.0,-0.4,0.2,2.4],x=[],y=[],i,j;for(i=0;icuts[j])cat=j+2;x.push(xv);y.push(cat);} var X=x.map(function(v){return[1,v];});var lin=lsq(X,y);var gaps=[];for(j=1;j1?'
    ':'')+'
    '+'
    '+'
    '+'
    '+'
    '+'
    ';var segBox=el.querySelector('.wf-seg'),title=el.querySelector('.wf-title'),ctl=el.querySelector('.wf-ctl'),plot=el.querySelector('.wf-plot'),fail=el.querySelector('.wf-fail'),read=el.querySelector('.wf-read'),rbox=el.querySelector('.wf-r');if(segBox){segBox.innerHTML=u.seg(list.map(function(k){return{v:k,label:MODES[k].short};}),which);u.wireSeg(segBox,function(v){which=v;render(true);});} function data(key){if(!cache[key]){var M=MODES[key];cache[key]=M.build(Math.max(10,+cfg.n||M.n),seed);} return cache[key];} function render(rebuildCtl){P=palette(el);el.style.background=P.panel;el.style.borderColor=P.line;title.style.color=P.mut;read.style.color=P.body;var M=MODES[which],D=data(which);title.textContent=M.title;if(rebuildCtl){ctl.innerHTML=M.slider?''+'':'';var sl=ctl.querySelector('.wf-deg');if(sl)sl.addEventListener('input',function(){deg=+sl.value;render(false);});} var dv=ctl.querySelector('.wf-dv');if(dv){dv.textContent=deg;dv.style.color=P.ink;} var out=VIEW[which](D,P,deg);plot.innerHTML=out.svg;fail.innerHTML=callout(out.fail,P);read.innerHTML=out.read;if(rebuildCtl||!rbox.firstChild)rbox.innerHTML=u.runnable(RCODE[which](D),{label:'Fit both models in R'});} var wasDark=isDark(el);if(window.MutationObserver){var mo=new MutationObserver(function(){if(!document.contains(el)){mo.disconnect();return;} var now=isDark(el);if(now!==wasDark){wasDark=now;render(true);}});mo.observe(document.documentElement,{attributes:true,attributeFilter:['class']});} render(true);} function callout(txt,P){return'
    '+txt+'
    ';} var W=520;function axes(P,m,iw,ih,xlab,ylab){return''+''+''+u.esc(xlab)+''+''+u.esc(ylab)+'';} function wrap(g,H,label){return''+g+'';} var VIEW={link:function(D,P){var m={l:42,r:16,t:14},iw=W-m.l-m.r,ih=190,H=236;var lo=-0.45,hi=1.45;function sx(v){return m.l+(v+3)/6*iw;} function sy(v){return m.t+(hi-v)/(hi-lo)*ih;} var g='';g+=''+''+'a probability cannot live up here'+'or down here';[0,0.5,1].forEach(function(v){g+=''+''+v+'';});D.x.forEach(function(v,i){g+='';});var pl=[],pg=[],t;for(t=-3;t<=3.0001;t+=0.05){pl.push(sx(t).toFixed(1)+','+sy(D.lin[0]+D.lin[1]*t).toFixed(1));pg.push(sx(t).toFixed(1)+','+sy(logistic(D.log[0]+D.log[1]*t)).toFixed(1));} g+=''+''+'logistic'+'linear model'+ axes(P,m,iw,ih,'predictor','outcome (0 or 1)');return{svg:wrap(g,H,'a linear model and a logistic model fitted to a binary outcome'),fail:''+D.bad+' of '+D.n+' fitted values from the linear model are impossible probabilities '+'(lowest '+D.worstLo.toFixed(2)+', highest '+D.worstHi.toFixed(2)+'). Ask it for the probability at x = '+D.outX+', one step past the data, and it answers '+D.outLin.toFixed(2)+'. The logistic model answers '+D.outLog.toFixed(2)+'.',read:'The straight line has to keep going. A probability cannot, so the model is forced to state things that have no meaning, '+'and it is worst exactly where the reviewer will look, at the two ends. The logistic curve is the same idea with a link that respects the range: '+'it bends and flattens instead of walking off the chart. Nothing was lost by switching, and the fitted values are now interpretable.'};},'zero-inflation':function(D,P){var m={l:42,r:16,t:22},iw=W-m.l-m.r,ih=180,H=234;var K=Math.min(12,D.maxC),i,j;var obs=new Array(K+1),exp=new Array(K+1);for(i=0;i<=K;i++){obs[i]=0;exp[i]=0;} for(i=0;i'+''+Math.round(v)+'';});for(i=0;i<=K;i++){var x0=m.l+bw*i;g+=''+''+''+ (i===K?i+'+':i)+'';} g+='solid = the data'+'outline = what the Poisson model expects'+ axes(P,m,iw,ih,'count','how many cases');return{svg:wrap(g,H,'observed counts against the counts a Poisson model expects'),fail:'The data contain '+D.obsZero+' zeros. The Poisson model expects '+D.expZero.toFixed(0)+'. '+'That gap is the model telling you it has the wrong shape.',read:'A Poisson distribution has one parameter, so its mean fixes how many zeros it can produce. Here the zeros come from two different places: '+ Math.round(100*D.pZero)+'% of the cases could never have had a positive count at all, and the rest could and sometimes did not. '+'One parameter cannot describe two processes, so the model splits the difference: it overstates the low counts it can reach and understates the zeros. '+'Fit a zero-inflated or a hurdle model and the two processes get a parameter each.'};},ordinal:function(D,P){var m={l:42,r:16,t:16},iw=W-m.l-m.r,ih=176,H=226;var lo=0.4,hi=5.9;function sx(v){return m.l+(v+2)/4*iw;} function sy(v){return m.t+(hi-v)/(hi-lo)*ih;} var g='',i,j;[1,2,3,4,5].forEach(function(v){g+=''+''+v+'';});var bins=5,cnt=[];for(i=0;i=0;j--){var frac=cnt[i][j]/tot,hgt=frac*(ih-12);g+='';if(frac>0.12)g+=''+(j+1)+'';yTop+=hgt;}} var pl=[],t;for(t=-2;t<=2.0001;t+=0.08)pl.push(sx(t).toFixed(1)+','+sy(D.lin[0]+D.lin[1]*t).toFixed(1));g+=''+'the numeric model'+'bars: what people actually answered in each slice of x'+ axes(P,m,iw,ih,'predictor','rating, 1 to 5');return{svg:wrap(g,H,'a straight line fitted to a five-point rating, against the observed answers'),fail:'The line predicts '+D.predLo.toFixed(2)+' at the bottom of the range and '+D.predHi.toFixed(2)+' at the top. Nobody gave those answers, because nobody could. Push it one step past the data, to x = '+D.outX+', and it predicts '+D.predOut.toFixed(2)+', which is off the scale entirely. '+'It also treats the step from 1 to 2 as equal to the step from 4 to 5; in the process behind this data they are '+ D.gaps[0].toFixed(1)+' and '+D.gaps[2].toFixed(1)+'.',read:'Rating scales are ordered, not spaced. The numbers 1 to 5 are labels for a ranking, and arithmetic on labels invents a measurement that was never taken. '+'Averaging them assumes every step is the same size, which is exactly the assumption the data violate here. '+'An ordered logistic model estimates one slope and lets the category boundaries sit wherever the data put them, which is the honest version of the same question.'};},overfitting:function(D,P,deg){var F=MODES.overfitting.refit(D,deg);var m={l:42,r:16,t:14},iw=W-m.l-m.r,ih=186,H=232;var lo=-2.2,hi=2.2;function sx(v){return m.l+(v+1.05)/2.1*iw;} function sy(v){return m.t+(hi-Math.max(lo,Math.min(hi,v)))/(hi-lo)*ih;} var g='',t;[-2,-1,0,1,2].forEach(function(v){g+=''+''+v+'';});var truth=[],curve=[];for(t=-1.05;t<=1.0501;t+=0.01){truth.push(sx(t).toFixed(1)+','+sy(Math.sin(2.3*t)).toFixed(1));curve.push(sx(t).toFixed(1)+','+sy(evalp(F.b,t)).toFixed(1));} g+='';D.xte.forEach(function(v,i){g+='';});D.xtr.forEach(function(v,i){g+='';});g+=''+'filled = training data'+'hollow = held out'+ axes(P,m,iw,ih,'predictor','outcome');var ratio=F.rmseTr>1e-9?F.rmseTe/F.rmseTr:Infinity;var offChart=false,tt;for(tt=-1.05;tt<=1.0501;tt+=0.02)if(Math.abs(evalp(F.b,tt))>hi)offChart=true;return{svg:wrap(g,H,'a polynomial fitted at the chosen degree, with training and held-out points'),fail:'Training R-squared '+F.r2.toFixed(3)+'. Error on the held-out points is '+ (isFinite(ratio)?ratio.toFixed(1)+'x':'unbounded')+' the training error ('+ F.rmseTe.toFixed(2)+' against '+F.rmseTr.toFixed(2)+').'+ (offChart?' The fitted curve runs off the top of the chart between the training points.':''),read:'Every extra term can only improve the training fit, so R-squared rising is not evidence of anything. '+'Watch the hollow points instead: they were never shown to the model, and they are the only honest test of whether it learned the pattern or the noise. '+ (deg<=4?'At this degree the curve is still tracking the real shape (dashed).':'At this degree the curve is chasing individual training points, and it swings hardest where the data are thinnest.')};},interaction:function(D,P){var m={l:42,r:16,t:14},iw=W-m.l-m.r,ih=190,H=236;var lo=-2,hi=14;function sx(v){return m.l+v/10*iw;} function sy(v){return m.t+(hi-Math.max(lo,Math.min(hi,v)))/(hi-lo)*ih;} var g='',t;[0,4,8,12].forEach(function(v){g+=''+''+v+'';});D.x.forEach(function(v,i){g+='';});function line(b,gi,col,dash){var a=b[0]+(b[2]||0)*gi,s=b[1]+(b.length>3?b[3]*gi:0);return'';} g+=line(D.ba,0,P.bad,true)+line(D.ba,1,P.bad,true)+line(D.bi,0,P.c0,false)+line(D.bi,1,P.c1,false);g+='dashed = additive model (parallel by construction)'+ axes(P,m,iw,ih,'predictor','outcome');var sA=D.bi[1],sB=D.bi[1]+D.bi[3];return{svg:wrap(g,H,'two groups with opposite slopes, fitted with and without an interaction'),fail:'The additive model reports one slope of '+D.ba[1].toFixed(2)+' for the predictor. '+'With the interaction in, the slope is '+sA.toFixed(2)+' in one group and '+sB.toFixed(2)+' in the other.',read:'An additive model can give the two groups different heights but not different directions: the two dashed lines are parallel because the model has no term that could make them anything else. '+'So it averages a rise and a fall and reports roughly nothing. The correct sentence is not that the predictor has no effect. It is that the effect depends on the group, which is a finding, and the model without the interaction term cannot express it.'};}};var RCODE={link:function(D){return['# A yes/no outcome, fitted the wrong way and then the right way.','set.seed(9)','n <- '+D.n,'x <- runif(n, -3, 3)','y <- rbinom(n, 1, plogis(1.4 * x))','','lin <- lm(y ~ x) # the wrong family','log <- glm(y ~ x, family = binomial) # the right one','','p_lin <- fitted(lin)','c(impossible = sum(p_lin < 0 | p_lin > 1),',' lowest = round(min(p_lin), 3),',' highest = round(max(p_lin), 3))','','range(fitted(log)) # a probability model stays inside 0 and 1'].join('\n');},'zero-inflation':function(D){return['# Counts with a pile of structural zeros, fitted as ordinary Poisson.','set.seed(9)','n <- '+D.n,'x <- rnorm(n)','y <- ifelse(runif(n) < '+D.pZero+', 0, rpois(n, exp(0.9 + 0.7 * x)))','','m <- glm(y ~ x, family = poisson)','mu <- fitted(m)','','c(observed_zeros = sum(y == 0),',' expected_zeros = round(sum(dpois(0, mu)), 1),',' poisson_slope = round(unname(coef(m)[2]), 3),',' true_slope = 0.7)','','# a zero-inflated model gives the two processes a parameter each','# install.packages("pscl"); pscl::zeroinfl(y ~ x | 1)'].join('\n');},ordinal:function(D){return['# A five-point rating, treated as a number and then as what it is.','set.seed(9)','n <- '+D.n,'x <- runif(n, -2, 2)','z <- 1.1 * x + rlogis(n)','y <- cut(z, c(-Inf, '+D.cuts.join(', ')+', Inf), labels = 1:5)','yn <- as.numeric(as.character(y)) # the mistake: a label used as a quantity','','lin <- lm(yn ~ x)','round(predict(lin, data.frame(x = c(-2, 2))), 2) # answers nobody can give','','table(y)','diff(c('+D.cuts.join(', ')+')) # the real gaps between categories','','# the honest model: one slope, boundaries wherever the data put them','summary(MASS::polr(y ~ x, Hess = TRUE))'].join('\n');},overfitting:function(D){return['# Flexibility that improves the training fit and nothing else.','set.seed(9)','n <- '+D.n+'; deg <- 12','xtr <- seq(-1, 1, length.out = n); ytr <- sin(2.3 * xtr) + rnorm(n, sd = 0.28)','xte <- runif(n, -1, 1); yte <- sin(2.3 * xte) + rnorm(n, sd = 0.28)','','fit <- lm(ytr ~ poly(xtr, deg, raw = TRUE))','pred <- function(x) as.numeric(cbind(1, outer(x, 1:deg, "^")) %*% coef(fit))','','round(c(train_r2 = summary(fit)$r.squared,',' train_rmse = sqrt(mean((ytr - pred(xtr))^2)),',' test_rmse = sqrt(mean((yte - pred(xte))^2))), 3)','','# raise deg and run it again: train_r2 only ever goes up','# test_rmse is the number that tells you when to stop'].join('\n');},interaction:function(D){return['# Two groups whose slopes point in opposite directions.','set.seed(9)','n <- '+D.n,'x <- runif(n, 0, 10)','g <- rep(0:1, length.out = n)','y <- ifelse(g == 1, 10 - 0.8 * x, 2 + 0.8 * x) + rnorm(n, sd = 1.5)','','add <- lm(y ~ x + g) # no interaction: the lines must stay parallel','int <- lm(y ~ x * g) # the slope is allowed to differ by group','','round(c(additive_slope = unname(coef(add)["x"]),',' slope_group0 = unname(coef(int)["x"]),',' slope_group1 = unname(coef(int)["x"] + coef(int)["x:g"])), 3)','','anova(add, int) # is the extra term earning its place?'].join('\n');}};window.LessonWidgets.register('wrong-family-fit',mount);})();