"""G15: group-disjoint splits, training-only scaling, and bounded Lloyd clustering."""importmathimportrandomdefsplit_groups(groups,seed=21):ifnotgroupsorany(type(g)isnotstrornotgforgingroups):raiseValueError("nonempty string group labels required")unique=sorted(set(groups))iflen(unique)<5:raiseValueError("at least five groups required for this split policy")random.Random(seed).shuffle(unique)a,b=max(1,int(.6*len(unique))),max(1,int(.2*len(unique)))partitions=[set(unique[:a]),set(unique[a:a+b]),set(unique[a+b:])]return{name:[ifori,ginenumerate(groups)ifginpart]forname,partinzip(("train","validation","test"),partitions)}defmatrix(X):ifnotXornotX[0]orlen(X)>10000orlen(X[0])>64:raiseValueError("bounded nonempty matrix required")d=len(X[0])ifany(len(r)!=dorany(notmath.isfinite(v)forvinr)forrinX):raiseValueError("finite rectangular features required")return[[float(v)forvinr]forrinX]deffit_scaler(X):X=matrix(X)n,d=len(X),len(X[0])mean=[math.fsum(r[j]forrinX)/nforjinrange(d)]scale=[math.sqrt(math.fsum((r[j]-mean[j])**2forrinX)/n)forjinrange(d)]ifnotall(math.isfinite(x)forxin[*mean,*scale]):raiseValueError("unsupported feature scale")return{"mean":mean,"scale":[sifs>0else1.forsinscale]}deftransform(X,scaler):X=matrix(X)mu,s=scaler["mean"],scaler["scale"]iflen(mu)!=len(X[0])orlen(s)!=len(mu)orany(v<=0forvins):raiseValueError("invalid scaler dimensions or scales")ifnotall(math.isfinite(v)forvin[*mu,*s]):raiseValueError("nonfinite scaler")return[[(x-m)/sdforx,m,sdinzip(row,mu,s)]forrowinX]defkmeans(X,initial,steps=50):X,C=matrix(X),matrix(initial)iflen(C)>len(X)orlen(C[0])!=len(X[0])ortype(steps)isnotintornot1<=steps<=1000:raiseValueError("matching cluster dimensions and bounded step count required")d=len(X[0])defassign(centers):return[min(range(len(centers)),key=lambdaj:math.fsum((a-b)**2fora,binzip(x,centers[j])))forxinX]history=[]for_inrange(steps):labels=assign(C)new=[]forjinrange(len(C)):members=[xforx,linzip(X,labels)ifl==j]new.append([math.fsum(x[k]forxinmembers)/len(members)forkinrange(d)]ifmemberselselist(C[j]))labels=assign(new)sse=math.fsum((v-c)**2forx,linzip(X,labels)forv,cinzip(x,new[l]))ifnotmath.isfinite(sse):raiseValueError("nonfinite clustering objective")history.append(sse)ifnew==C:C=newbreakC=newreturn{"centers":C,"labels":labels,"sse":history[-1],"history":history}deffixture(seed=21,n_groups=30):rng=random.Random(seed)rows=[]forginrange(n_groups):group_effect=rng.uniform(-.3,.3)forrinrange(4):x=[rng.uniform(-2,2),rng.uniform(-2,2)]noise=rng.uniform(-.4,.4)rows.append({"group":f"g{g:03}","x":x,"y":int(x[0]+.6*x[1]+group_effect+noise>0)})returnrowsif__name__=="__main__":rows=fixture()print({k:len(v)fork,vinsplit_groups([r["group"]forrinrows]).items()})print(kmeans([[0.],[2.],[8.],[10.]],[[0.],[10.]]))